From ea20030adabe79d8ee55b889f7381ae75c164981 Mon Sep 17 00:00:00 2001 From: Daan Rosendal Date: Thu, 7 Dec 2023 22:52:45 +0100 Subject: [PATCH 1/4] feat(ui): add dropdowns for filtering shared workflows (#375) Closes #367 --- AUTHORS.md | 1 + reana-ui/src/actions.js | 55 +++++++ reana-ui/src/client.js | 30 +++- .../src/pages/workflowList/WorkflowList.js | 24 ++- .../workflowList/WorkflowList.module.scss | 37 ++++- .../components/WorkflowFilters.js | 19 ++- .../components/WorkflowSharingFilter.js | 153 ++++++++++++++++++ .../WorkflowSharingFilter.module.scss | 46 ++++++ .../components/WorkflowStatusFilter.js | 11 +- reana-ui/src/reducers.js | 39 +++++ reana-ui/src/selectors.js | 6 + 11 files changed, 412 insertions(+), 9 deletions(-) create mode 100644 reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.js create mode 100644 reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.module.scss diff --git a/AUTHORS.md b/AUTHORS.md index d9dbe022..a1068b2a 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -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) diff --git a/reana-ui/src/actions.js b/reana-ui/src/actions.js index b26c69f2..eb32d594 100644 --- a/reana-ui/src/actions.js +++ b/reana-ui/src/actions.js @@ -91,6 +91,19 @@ export const OPEN_INTERACTIVE_SESSION_MODAL = "Open interactive session modal"; export const CLOSE_INTERACTIVE_SESSION_MODAL = "Close interactive session modal"; +export const USERS_SHARED_WITH_YOU_FETCH = + "Fetch users who shared workflows with you"; +export const USERS_SHARED_WITH_YOU_RECEIVED = + "Users who shared workflows with you received"; +export const USERS_SHARED_WITH_YOU_FETCH_ERROR = + "Fetch users who shared workflows with you error"; +export const USERS_YOU_SHARED_WITH_FETCH = + "Fetch users you shared workflows with"; +export const USERS_YOU_SHARED_WITH_RECEIVED = + "Users you shared workflows with received"; +export const USERS_YOU_SHARED_WITH_FETCH_ERROR = + "Fetch users you shared workflows with error"; + export function errorActionCreator(error, name) { const { status, data } = error?.response; const { message } = data; @@ -262,6 +275,8 @@ export function fetchWorkflows({ pagination, search, status, + ownedBy, + sharedWith, sort, showLoader = true, workflowIdOrName, @@ -275,6 +290,8 @@ export function fetchWorkflows({ pagination, search: formatSearch(search), status, + ownedBy, + sharedWith, sort, workflowIdOrName, }) @@ -522,3 +539,41 @@ export function closeInteractiveSession(id) { }); }; } + +export function fetchUsersSharedWithYou() { + return async (dispatch) => { + dispatch({ type: USERS_SHARED_WITH_YOU_FETCH }); + + return await client + .getUsersSharedWithYou() + .then((resp) => { + dispatch({ + type: USERS_SHARED_WITH_YOU_RECEIVED, + users_shared_with_you: resp.data.users_shared_with_you, + }); + return resp; + }) + .catch((err) => { + dispatch(errorActionCreator(err, USERS_SHARED_WITH_YOU_FETCH_ERROR)); + }); + }; +} + +export function fetchUsersYouSharedWith() { + return async (dispatch) => { + dispatch({ type: USERS_YOU_SHARED_WITH_FETCH }); + + return await client + .getUsersYouSharedWith() + .then((resp) => { + dispatch({ + type: USERS_YOU_SHARED_WITH_RECEIVED, + users_you_shared_with: resp.data.users_you_shared_with, + }); + return resp; + }) + .catch((err) => { + dispatch(errorActionCreator(err, USERS_YOU_SHARED_WITH_FETCH_ERROR)); + }); + }; +} diff --git a/reana-ui/src/client.js b/reana-ui/src/client.js index 6b3049e2..2d0ad80d 100644 --- a/reana-ui/src/client.js +++ b/reana-ui/src/client.js @@ -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) => @@ -116,13 +118,31 @@ class Client { }); } - getWorkflows({ pagination, search, status, sort, workflowIdOrName } = {}) { + getWorkflows({ + pagination, + search, + status, + ownedBy, + sharedWith, + sort, + workflowIdOrName, + } = {}) { + let shared = false; + if (ownedBy === "anybody") { + ownedBy = undefined; + shared = true; + } else if (ownedBy === "you") { + ownedBy = undefined; + } return this._request( WORKFLOWS_URL({ ...(pagination ?? {}), workflow_id_or_name: workflowIdOrName, search, status, + shared, + shared_by: ownedBy, + shared_with: sharedWith, sort, }), ); @@ -198,6 +218,14 @@ class Client { method: "post", }); } + + getUsersSharedWithYou() { + return this._request(USERS_SHARED_WITH_YOU_URL); + } + + getUsersYouSharedWith() { + return this._request(USERS_YOU_SHARED_WITH_URL); + } } const client = new Client(); diff --git a/reana-ui/src/pages/workflowList/WorkflowList.js b/reana-ui/src/pages/workflowList/WorkflowList.js index a442e426..ab053841 100644 --- a/reana-ui/src/pages/workflowList/WorkflowList.js +++ b/reana-ui/src/pages/workflowList/WorkflowList.js @@ -2,7 +2,7 @@ -*- coding: utf-8 -*- This file is part of REANA. - Copyright (C) 2020, 2021, 2022 CERN. + Copyright (C) 2020, 2021, 2022, 2023 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. @@ -51,6 +51,8 @@ function Workflows() { const [pagination, setPagination] = useState({ page: 1, size: PAGE_SIZE }); const [statusFilter, setStatusFilter] = useState(NON_DELETED_STATUSES); const [searchFilter, setSearchFilter] = useState(); + const [ownedByFilter, setOwnedByFilter] = useState(); + const [sharedWithFilter, setSharedWithFilter] = useState(); const [sortDir, setSortDir] = useState("desc"); const dispatch = useDispatch(); const config = useSelector(getConfig); @@ -76,6 +78,8 @@ function Workflows() { pagination: { ...pagination }, search: searchFilter, status: statusFilter, + ownedBy: ownedByFilter, + sharedWith: sharedWithFilter, sort: sortDir, }), ); @@ -88,6 +92,8 @@ function Workflows() { pagination: { ...pagination }, search: searchFilter, status: statusFilter, + ownedBy: ownedByFilter, + sharedWith: sharedWithFilter, sort: sortDir, showLoader, }), @@ -103,6 +109,8 @@ function Workflows() { reanaToken, searchFilter, statusFilter, + ownedByFilter, + sharedWithFilter, sortDir, workflowRefresh, ]); @@ -133,7 +141,7 @@ function Workflows() { return (
- + <span>Your workflows</span> <span className={styles.refresh}> @@ -155,6 +163,18 @@ function Workflows() { pagination, setPagination, )} + ownedByFilter={ownedByFilter} + setOwnedByFilter={applyFilter( + setOwnedByFilter, + pagination, + setPagination, + )} + sharedWithFilter={sharedWithFilter} + setSharedWithFilter={applyFilter( + setSharedWithFilter, + pagination, + setPagination, + )} sortDir={sortDir} setSortDir={applyFilter(setSortDir, pagination, setPagination)} /> diff --git a/reana-ui/src/pages/workflowList/WorkflowList.module.scss b/reana-ui/src/pages/workflowList/WorkflowList.module.scss index 298dff09..44699bd7 100644 --- a/reana-ui/src/pages/workflowList/WorkflowList.module.scss +++ b/reana-ui/src/pages/workflowList/WorkflowList.module.scss @@ -2,7 +2,7 @@ -*- coding: utf-8 -*- This file is part of REANA. - Copyright (C) 2020, 2022 CERN. + Copyright (C) 2020, 2022, 2023 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. @@ -41,3 +41,38 @@ :global(.ui.pagination.menu).pagination { margin: 2em; } + +#workflow-list-container { + font-size: 1.14285714rem; + line-height: 1.5; + margin-left: 0; + margin-right: 0; +} + +/* Mobile */ +@media only screen and (max-width: 767px) { + #workflow-list-container { + width: 500px; + } +} + +/* Tablet */ +@media only screen and (min-width: 768px) and (max-width: 991px) { + #workflow-list-container { + width: 700px; + } +} + +/* Small Monitor */ +@media only screen and (min-width: 992px) and (max-width: 1199px) { + #workflow-list-container { + width: 800px; + } +} + +/* Large Monitor */ +@media only screen and (min-width: 1200px) { + #workflow-list-container { + width: 825px; + } +} diff --git a/reana-ui/src/pages/workflowList/components/WorkflowFilters.js b/reana-ui/src/pages/workflowList/components/WorkflowFilters.js index 1c5c6074..98b8a800 100644 --- a/reana-ui/src/pages/workflowList/components/WorkflowFilters.js +++ b/reana-ui/src/pages/workflowList/components/WorkflowFilters.js @@ -2,7 +2,7 @@ -*- coding: utf-8 -*- This file is part of REANA. - Copyright (C) 2020, 2022 CERN. + Copyright (C) 2020, 2022, 2023 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. @@ -15,12 +15,17 @@ import WorkflowStatusFilter from "./WorkflowStatusFilter"; import WorkflowSorting from "./WorkflowSorting"; import styles from "./WorkflowFilters.module.scss"; +import WorkflowSharingFilters from "./WorkflowSharingFilter"; export default function WorkflowFilters({ statusFilter, setStatusFilter, sortDir, setSortDir, + ownedByFilter, + setOwnedByFilter, + sharedWithFilter, + setSharedWithFilter, }) { return ( <div className={styles.container}> @@ -29,7 +34,13 @@ export default function WorkflowFilters({ statusFilter={statusFilter} filter={setStatusFilter} /> - <Grid.Column floated="right" width={4}> + <WorkflowSharingFilters + ownedByFilter={ownedByFilter} + setOwnedByFilter={setOwnedByFilter} + sharedWithFilter={sharedWithFilter} + setSharedWithFilter={setSharedWithFilter} + /> + <Grid.Column mobile={16} tablet={4} computer={3} floated="right"> <WorkflowSorting value={sortDir} sort={setSortDir} /> </Grid.Column> </Grid> @@ -42,4 +53,8 @@ WorkflowFilters.propTypes = { setStatusFilter: PropTypes.func.isRequired, sortDir: PropTypes.string.isRequired, setSortDir: PropTypes.func.isRequired, + ownedByFilter: PropTypes.string, + setOwnedByFilter: PropTypes.func.isRequired, + sharedWithFilter: PropTypes.string, + setSharedWithFilter: PropTypes.func.isRequired, }; diff --git a/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.js b/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.js new file mode 100644 index 00000000..d8632d82 --- /dev/null +++ b/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.js @@ -0,0 +1,153 @@ +/* + -*- coding: utf-8 -*- + + This file is part of REANA. + Copyright (C) 2023 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 _, { isEqual } from "lodash"; +import PropTypes from "prop-types"; +import { useEffect, useMemo, useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { Dropdown, Grid } from "semantic-ui-react"; +import { fetchUsersSharedWithYou, fetchUsersYouSharedWith } from "~/actions"; +import { getUsersSharedWithYou, getUsersYouSharedWith } from "~/selectors"; +import styles from "./WorkflowSharingFilter.module.scss"; + +const sharingFilterOptions = [ + { key: 0, text: "Owned by", value: "owned_by" }, + { key: 1, text: "Shared with", value: "shared_with" }, +]; + +export default function WorkflowSharingFilters({ + ownedByFilter, + setOwnedByFilter, + sharedWithFilter, + setSharedWithFilter, +}) { + const dispatch = useDispatch(); + const [selectedFilterOption, setSelectedFilterOption] = useState( + sharingFilterOptions[0], + ); + const [dynamicOptions, setDynamicOptions] = useState([]); + + const usersYouSharedWith = useSelector(getUsersYouSharedWith, _.isEqual); + const usersSharedWithYou = useSelector(getUsersSharedWithYou, _.isEqual); + + const usersSharedWithYouOptions = useMemo( + () => [ + { key: "you", text: "you", value: "you" }, + { key: "anybody", text: "anybody", value: "anybody" }, + ...usersSharedWithYou.map((user, index) => ({ + key: index, + text: user.email, + value: user.email, + })), + ], + [usersSharedWithYou], + ); + + const usersYouSharedWithOptions = useMemo( + () => [ + { key: "anybody", text: "anybody", value: "anybody" }, + ...usersYouSharedWith.map((user, index) => ({ + key: index, + text: user.email, + value: user.email, + })), + ], + [usersYouSharedWith], + ); + + const [selectedUser, setSelectedUser] = useState(); + + useEffect(() => { + dispatch(fetchUsersYouSharedWith()); + dispatch(fetchUsersSharedWithYou()); + }, [dispatch]); + + useEffect(() => { + setDynamicOptions(usersSharedWithYouOptions); + }, [usersSharedWithYou, usersSharedWithYouOptions]); + + useEffect(() => { + if (selectedFilterOption.value === sharingFilterOptions[0].value) { + if (!isEqual(ownedByFilter, selectedUser?.value)) { + setSharedWithFilter(undefined); + setOwnedByFilter(selectedUser?.value); + } + } else { + if (!isEqual(sharedWithFilter, selectedUser?.value)) { + setOwnedByFilter(undefined); + setSharedWithFilter(selectedUser?.value); + } + } + }, [ + ownedByFilter, + sharedWithFilter, + setOwnedByFilter, + setSharedWithFilter, + selectedUser, + selectedFilterOption.value, + ]); + + const handleSelectedFilterOptionChange = (_, { value }) => { + const selectedOption = sharingFilterOptions.find( + (option) => option.value === value, + ); + setSelectedFilterOption(selectedOption); + + if (value === sharingFilterOptions[0].value) { + setDynamicOptions(usersSharedWithYouOptions); + setSelectedUser(usersSharedWithYouOptions[0]); + } else { + setDynamicOptions(usersYouSharedWithOptions); + setSelectedUser(usersYouSharedWithOptions[0]); + } + }; + + const handleSelectedUserChange = (_, { value }) => { + const selectedUser = dynamicOptions.find( + (option) => option.value === value, + ); + setSelectedUser(selectedUser); + }; + + return ( + <Grid.Column mobile={16} tablet={7} computer={6}> + <div style={{ display: "flex" }}> + <Dropdown + text={selectedFilterOption.text} + fluid + closeOnChange + selection + options={sharingFilterOptions} + onChange={handleSelectedFilterOptionChange} + defaultValue={selectedFilterOption.value} + id={styles["sharing-filter-dropdown"]} + /> + <Dropdown + text={selectedUser?.text || "you"} + fluid + selection + search + scrolling + options={dynamicOptions} + onChange={handleSelectedUserChange} + value={selectedUser?.value || "you"} + id={styles["selected-user-dropdown"]} + /> + </div> + </Grid.Column> + ); +} + +WorkflowSharingFilters.propTypes = { + ownedByFilter: PropTypes.string, + setOwnedByFilter: PropTypes.func.isRequired, + sharedWithFilter: PropTypes.string, + setSharedWithFilter: PropTypes.func.isRequired, +}; diff --git a/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.module.scss b/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.module.scss new file mode 100644 index 00000000..979d8bed --- /dev/null +++ b/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.module.scss @@ -0,0 +1,46 @@ +/* + -*- coding: utf-8 -*- + + This file is part of REANA. + Copyright (C) 2023 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. +*/ + +#sharing-filter-dropdown { + border-top-right-radius: 0%; + border-bottom-right-radius: 0%; +} + +#sharing-filter-dropdown:hover, +#sharing-filter-dropdown:focus, +#sharing-filter-dropdown:active { + z-index: 2; +} + +#selected-user-dropdown { + border-top-left-radius: 0%; + border-bottom-left-radius: 0%; + margin-left: -1px; + overflow: hidden; +} + +#selected-user-dropdown.input { + border-top-left-radius: 0%; + border-bottom-left-radius: 0%; + margin-left: -1px; +} + +#selected-user-dropdown > div[aria-atomic="true"] { + max-width: 105px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + height: 14+2px; + margin-bottom: -2px; +} + +#selected-user-dropdown[aria-expanded="true"] { + overflow: visible; +} diff --git a/reana-ui/src/pages/workflowList/components/WorkflowStatusFilter.js b/reana-ui/src/pages/workflowList/components/WorkflowStatusFilter.js index a31b7755..99f6e19d 100644 --- a/reana-ui/src/pages/workflowList/components/WorkflowStatusFilter.js +++ b/reana-ui/src/pages/workflowList/components/WorkflowStatusFilter.js @@ -2,7 +2,7 @@ -*- coding: utf-8 -*- This file is part of REANA. - Copyright (C) 2020, 2022 CERN. + Copyright (C) 2020, 2022, 2023 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. @@ -49,7 +49,7 @@ export default function WorkflowStatusFilters({ statusFilter, filter }) { return ( <> - <Grid.Column width={7}> + <Grid.Column mobile={16} tablet={4} computer={3}> <Dropdown text="Status" fluid @@ -61,7 +61,12 @@ export default function WorkflowStatusFilters({ statusFilter, filter }) { value={valueList} /> </Grid.Column> - <Grid.Column width={5}> + <Grid.Column + mobile={16} + tablet={5} + computer={4} + className="center aligned" + > <Checkbox toggle label="Show deleted runs" diff --git a/reana-ui/src/reducers.js b/reana-ui/src/reducers.js index a7ec186e..0eba6c0e 100644 --- a/reana-ui/src/reducers.js +++ b/reana-ui/src/reducers.js @@ -45,6 +45,12 @@ import { OPEN_STOP_WORKFLOW_MODAL, CLOSE_STOP_WORKFLOW_MODAL, WARNING, + USERS_SHARED_WITH_YOU_FETCH, + USERS_SHARED_WITH_YOU_RECEIVED, + USERS_SHARED_WITH_YOU_FETCH_ERROR, + USERS_YOU_SHARED_WITH_FETCH, + USERS_YOU_SHARED_WITH_RECEIVED, + USERS_YOU_SHARED_WITH_FETCH_ERROR, } from "~/actions"; import { USER_ERROR } from "./errors"; @@ -106,6 +112,11 @@ const quotaInitialState = { disk: {}, }; +const sharingInitialState = { + usersSharedWithYou: [], + usersYouSharedWith: [], +}; + const notification = (state = notificationInitialState, action) => { const { name, status, message, header } = action; switch (action.type) { @@ -357,6 +368,33 @@ const quota = (state = quotaInitialState, action) => { } }; +const sharing = (state = sharingInitialState, action) => { + switch (action.type) { + case USERS_SHARED_WITH_YOU_FETCH: + return { ...state }; + case USERS_SHARED_WITH_YOU_RECEIVED: + return { + ...state, + usersSharedWithYou: action.users_shared_with_you, + }; + case USERS_SHARED_WITH_YOU_FETCH_ERROR: + return { ...state, loading: false }; + case USERS_YOU_SHARED_WITH_FETCH: + return { + ...state, + }; + case USERS_YOU_SHARED_WITH_RECEIVED: + return { + ...state, + usersYouSharedWith: action.users_you_shared_with, + }; + case USERS_YOU_SHARED_WITH_FETCH_ERROR: + return { ...state, loading: false }; + default: + return state; + } +}; + const reanaApp = combineReducers({ notification, config, @@ -364,6 +402,7 @@ const reanaApp = combineReducers({ workflows, details, quota, + sharing, }); export default reanaApp; diff --git a/reana-ui/src/selectors.js b/reana-ui/src/selectors.js index ac9176b8..15b87f50 100644 --- a/reana-ui/src/selectors.js +++ b/reana-ui/src/selectors.js @@ -71,3 +71,9 @@ export const getWorkflowParameters = (id) => (state) => id in state.details.details && state.details.details[id].parameters; export const getWorkflowRetentionRules = (id) => (state) => id in state.details.details && state.details.details[id].retentionRules; + +// Sharing +export const getUsersSharedWithYou = (state) => + state.sharing && state.sharing.usersSharedWithYou; +export const getUsersYouSharedWith = (state) => + state.sharing && state.sharing.usersYouSharedWith; From 7722ff385d24680d1c8fc3007c7fe65a94b48974 Mon Sep 17 00:00:00 2001 From: Daan Rosendal <daan.rosendal@cern.ch> Date: Tue, 12 Dec 2023 19:08:48 +0100 Subject: [PATCH 2/4] feat(ui): add workflow sharing modal (#375) Closes #651 --- reana-ui/package.json | 1 + reana-ui/src/actions.js | 108 ++++ reana-ui/src/client.js | 23 + .../src/components/WorkflowActionsPopup.js | 12 + reana-ui/src/components/WorkflowShareModal.js | 490 +++++++++++++++++ .../components/WorkflowShareModal.module.scss | 100 ++++ reana-ui/src/components/index.js | 1 + .../pages/workflowDetails/WorkflowDetails.js | 2 + .../workflowList/components/WorkflowList.js | 2 + reana-ui/src/reducers.js | 89 +++- reana-ui/src/selectors.js | 23 + reana-ui/yarn.lock | 500 ++++++++++-------- 12 files changed, 1143 insertions(+), 208 deletions(-) create mode 100644 reana-ui/src/components/WorkflowShareModal.js create mode 100644 reana-ui/src/components/WorkflowShareModal.module.scss diff --git a/reana-ui/package.json b/reana-ui/package.json index e5f80885..74b854d6 100644 --- a/reana-ui/package.json +++ b/reana-ui/package.json @@ -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", diff --git a/reana-ui/src/actions.js b/reana-ui/src/actions.js index eb32d594..ad25e5f2 100644 --- a/reana-ui/src/actions.js +++ b/reana-ui/src/actions.js @@ -86,10 +86,23 @@ 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 WORKFLOW_SHARE_INIT = "Initialise workflow sharing"; +export const WORKFLOW_SHARED_SUCCESSFULLY = "Workflow shared successfully"; +export const WORKFLOW_SHARED_ERROR = "Workflow shared error"; +export const WORKFLOW_SHARE_FINISH = "Finish workflow sharing"; +export const WORKFLOW_UNSHARE_INIT = "Initialise workflow unsharing"; +export const WORKFLOW_UNSHARED = "Workflow unshared"; +export const WORKFLOW_UNSHARE_ERROR = "Workflow unshare error"; export const USERS_SHARED_WITH_YOU_FETCH = "Fetch users who shared workflows with you"; @@ -540,6 +553,14 @@ 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) => { dispatch({ type: USERS_SHARED_WITH_YOU_FETCH }); @@ -577,3 +598,90 @@ export function fetchUsersYouSharedWith() { }); }; } + +export function fetchWorkflowShareStatus(id) { + return async (dispatch) => { + dispatch({ type: WORKFLOW_SHARE_STATUS_FETCH }); + return await client + .getWorkflowShareStatus(id) + .then((resp) => { + dispatch({ + type: WORKFLOW_SHARE_STATUS_RECEIVED, + id, + workflow_share_status: resp.data.shared_with, + }); + return resp; + }) + .catch((err) => { + dispatch(errorActionCreator(err, WORKFLOW_SHARE_STATUS_FETCH_ERROR)); + }); + }; +} + +export function shareWorkflow( + id, + user_id, + user_emails_to_share_with, + valid_until, +) { + return async (dispatch) => { + dispatch({ type: WORKFLOW_SHARE_INIT }); + + const users_shared_with = []; + const users_not_shared_with = []; + + for (const user_email_to_share_with of user_emails_to_share_with) { + await client + .shareWorkflow(id, { + user_id, + user_email_to_share_with, + valid_until, + }) + .then(() => { + users_shared_with.push(user_email_to_share_with); + }) + .catch((err) => { + const error_message = err.response.data.message; + users_not_shared_with.push({ + user_email_to_share_with, + error_message, + }); + }); + + if (users_shared_with.length > 0) { + dispatch({ + type: WORKFLOW_SHARED_SUCCESSFULLY, + users_shared_with, + }); + } + + if (users_not_shared_with.length > 0) { + dispatch({ + type: WORKFLOW_SHARED_ERROR, + users_not_shared_with, + }); + } + } + + dispatch({ type: WORKFLOW_SHARE_FINISH }); + }; +} + +export function unshareWorkflow(id, user_id, user_email_to_unshare_with) { + return async (dispatch) => { + dispatch({ type: WORKFLOW_UNSHARE_INIT }); + + return await client + .unshareWorkflow(id, { + user_id, + user_email_to_unshare_with, + }) + .then(() => { + dispatch({ type: WORKFLOW_UNSHARED, user_email_to_unshare_with }); + }) + .catch((err) => { + const error_message = err.response.data.message; + dispatch({ type: WORKFLOW_UNSHARE_ERROR, error_message }); + }); + }; +} diff --git a/reana-ui/src/client.js b/reana-ui/src/client.js index 2d0ad80d..9d508679 100644 --- a/reana-ui/src/client.js +++ b/reana-ui/src/client.js @@ -45,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) => @@ -226,6 +231,24 @@ class Client { getUsersYouSharedWith() { return this._request(USERS_YOU_SHARED_WITH_URL); } + + getWorkflowShareStatus(id) { + return this._request(WORKFLOW_SHARE_STATUS_URL(id)); + } + + shareWorkflow(id, data) { + return this._request(WORKFLOW_SHARE_URL(id), { + data, + method: "post", + }); + } + + unshareWorkflow(id, data) { + return this._request(WORKFLOW_UNSHARE_URL(id), { + data, + method: "post", + }); + } } const client = new Client(); diff --git a/reana-ui/src/components/WorkflowActionsPopup.js b/reana-ui/src/components/WorkflowActionsPopup.js index 05653438..3ecf5bc5 100644 --- a/reana-ui/src/components/WorkflowActionsPopup.js +++ b/reana-ui/src/components/WorkflowActionsPopup.js @@ -20,6 +20,7 @@ import { openDeleteWorkflowModal, openStopWorkflowModal, openInteractiveSessionModal, + openShareWorkflowModal, } from "~/actions"; import { JupyterNotebookIcon } from "~/components"; @@ -51,6 +52,17 @@ export default function WorkflowActionsPopup({ workflow, className }) { }); } + menuItems.push({ + key: "share", + content: "Share workflow", + icon: "share alternate", + onClick: (e) => { + dispatch(openShareWorkflowModal(workflow)); + setOpen(false); + e.stopPropagation(); + }, + }); + if (isSessionOpen) { menuItems.push({ key: "closeNotebook", diff --git a/reana-ui/src/components/WorkflowShareModal.js b/reana-ui/src/components/WorkflowShareModal.js new file mode 100644 index 00000000..8bdc7562 --- /dev/null +++ b/reana-ui/src/components/WorkflowShareModal.js @@ -0,0 +1,490 @@ +/* + -*- coding: utf-8 -*- + + This file is part of REANA. + Copyright (C) 2023 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 { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + Button, + Modal, + Icon, + Popup, + Checkbox, + Dropdown, + TextArea, + Loader, + Message, + Confirm, +} from "semantic-ui-react"; + +import { + closeShareWorkflowModal, + fetchWorkflowShareStatus, + shareWorkflow, + unshareWorkflow, +} from "~/actions"; +import { + getLoadingWorkflowShare, + getLoadingWorkflowShareStatus, + getLoadingWorkflowUnshare, + getUnshareError, + getUserId, + getUserWorkflowWasUnsharedWith, + getUsersWorkflowWasNotSharedWith, + getUsersWorkflowWasSharedWith, + getWorkflowShareModalItem, + getWorkflowShareModalOpen, + getWorkflowShareStatus, +} from "~/selectors"; + +import styles from "./WorkflowShareModal.module.scss"; +import SemanticDatepicker from "react-semantic-ui-datepickers"; + +export default function WorkflowShareModal() { + const dispatch = useDispatch(); + const open = useSelector(getWorkflowShareModalOpen); + const workflow = useSelector(getWorkflowShareModalItem); + const [linkCopied, setLinkCopied] = useState(false); + const [dropDownOptions, setDropDownOptions] = useState([]); + const [usersToShareWith, setUsersToShareWith] = useState([]); + const [expirationModalOpen, setExpirationModalOpen] = useState(false); + const [expirationDate, setExpirationDate] = useState(null); + const [neverExpires, setNeverExpires] = useState(true); + const [workflowShares, setWorkflowShares] = useState([]); + const workflowShareStatus = useSelector(getWorkflowShareStatus(workflow?.id)); + const [workflowShareStatusLoading, setWorkflowShareStatusLoading] = + useState(false); + const loadingWorkflowShareStatus = useSelector(getLoadingWorkflowShareStatus); + + const loadingWorkflowShare = useSelector(getLoadingWorkflowShare); + const loadingWorkflowUnshare = useSelector(getLoadingWorkflowUnshare); + const [unshareError, setUnshareError] = useState(null); + const workflowUnshareError = useSelector(getUnshareError); + const [usersSharedWith, setUsersSharedWith] = useState([]); + const usersWorkflowWasSharedWith = useSelector(getUsersWorkflowWasSharedWith); + const [usersNotSharedWith, setUsersNotSharedWith] = useState([]); + const usersWorkflowWasNotSharedWith = useSelector( + getUsersWorkflowWasNotSharedWith, + ); + const [confirmUnshareOpen, setConfirmUnshareOpen] = useState(false); + const [userToUnshareWith, setUserToUnshareWith] = useState(null); + const [userUnsharedWith, setUserUnsharedWith] = useState(null); + const userWorkflowWasUnsharedWith = useSelector( + getUserWorkflowWasUnsharedWith, + ); + const userId = useSelector(getUserId); + + const resetMessages = () => { + setUsersSharedWith([]); + setUsersNotSharedWith([]); + setUserUnsharedWith(null); + setUnshareError(null); + }; + + useEffect(() => { + resetMessages(); + }, [open]); + + useEffect(() => { + if (workflow) { + dispatch(fetchWorkflowShareStatus(workflow.id)); + } + }, [dispatch, workflow]); + + useEffect(() => { + if (!workflowShareStatus) return; + setWorkflowShares(workflowShareStatus); + }, [workflowShareStatus]); + + useEffect(() => { + setWorkflowShareStatusLoading(loadingWorkflowShareStatus); + }, [loadingWorkflowShareStatus]); + + useEffect(() => { + if (!loadingWorkflowShare) { + setUsersSharedWith(usersWorkflowWasSharedWith); + setUsersNotSharedWith(usersWorkflowWasNotSharedWith); + setUserUnsharedWith(null); + setUsersToShareWith([]); + if (workflow) dispatch(fetchWorkflowShareStatus(workflow.id)); + } + // disable eslint because we want to run this effect only when + // loadingWorkflowShare changes, for further context: + // https://github.com/facebook/create-react-app/issues/6880#issuecomment-485963251 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [loadingWorkflowShare]); + + useEffect(() => { + if (!loadingWorkflowUnshare) { + if (workflow) dispatch(fetchWorkflowShareStatus(workflow.id)); + + if (!workflowUnshareError) { + setUserUnsharedWith(userWorkflowWasUnsharedWith); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [loadingWorkflowUnshare]); + + useEffect(() => { + setUnshareError(workflowUnshareError); + if (workflowUnshareError) { + setUsersSharedWith([]); + setUsersNotSharedWith([]); + if (workflow) dispatch(fetchWorkflowShareStatus(workflow.id)); + } + }, [workflowUnshareError, dispatch, workflow]); + + if (!workflow) return null; + + const onCloseModal = () => { + dispatch(closeShareWorkflowModal()); + resetMessages(); + }; + + const copyCurrentUrl = () => { + navigator.clipboard.writeText(window.location.href); + setLinkCopied(true); + + setTimeout(() => { + setLinkCopied(false); + }, 3000); + }; + + const handleAddition = (e, { value }) => { + setDropDownOptions((prevOptions) => [ + { text: value, value }, + ...prevOptions, + ]); + }; + + const handleChange = (e, { value }) => { + setUsersToShareWith(value); + }; + + const openExpirationModal = () => { + setExpirationModalOpen(true); + }; + + const closeExpirationModal = () => { + setExpirationModalOpen(false); + }; + + const handleChangeExpirationDate = (_, data) => setExpirationDate(data.value); + + const handleSetExpirationDate = () => { + if (neverExpires || !expirationDate) { + setExpirationDate(null); + setNeverExpires(true); + } else { + setExpirationDate(expirationDate); + } + setExpirationModalOpen(false); + }; + + const handleShareWorkflow = () => { + resetMessages(); + if (usersToShareWith.length === 0) { + return; + } + + dispatch( + shareWorkflow( + workflow.id, + userId, + usersToShareWith, + expirationDate?.toLocaleDateString("sv-SE", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }), + ), + ); + }; + + const handleUnshareWorkflow = (userToUnshareWith) => { + setConfirmUnshareOpen(false); + setUsersSharedWith([]); + setUsersNotSharedWith([]); + setUserUnsharedWith(null); + dispatch(unshareWorkflow(workflow.id, userId, userToUnshareWith)); + }; + + const { name, run } = workflow; + return ( + <Modal + open={open} + onClose={onCloseModal} + closeIcon + size="tiny" + id={styles["workflow-share-modal"]} + > + <Modal.Header> + Share {name} #{run} + </Modal.Header> + + <Modal.Content> + <Dropdown + options={dropDownOptions} + placeholder="Emails of users to share with" + search + selection + fluid + allowAdditions + multiple + noResultsMessage={null} + value={usersToShareWith} + onAddItem={handleAddition} + onChange={handleChange} + disabled={loadingWorkflowShare} + /> + + <div className="ui form" id={styles["message"]}> + <TextArea placeholder="Message (optional)" /> + </div> + <div id={styles["share-buttons"]}> + <div id={styles["share-button"]}> + <Popup + content={ + "Please provide at least one user to share the workflow with." + } + position="top center" + disabled={usersToShareWith.length > 0} + trigger={ + <span> + <Button + primary + icon + labelPosition="left" + onClick={() => { + handleShareWorkflow(); + }} + disabled={usersToShareWith.length === 0} + > + <Icon name="share alternate" /> + Share + </Button> + </span> + } + /> + + {loadingWorkflowShare && ( + <Loader inline id={styles["loader-share-workflow"]} inverted /> + )} + </div> + <Popup + content={"Expires"} + position="top center" + trigger={ + <Button + onClick={openExpirationModal} + icon + labelPosition="left" + id={styles["expiration-date-button"]} + > + <Icon name="clock" /> + {neverExpires || !expirationDate + ? "Never" + : expirationDate?.toLocaleDateString("sv-SE", { + year: "numeric", + month: "2-digit", + day: "2-digit", + })} + </Button> + } + /> + </div> + + {usersSharedWith.length > 0 && ( + <Message success> + <Message.Header> + {name} #{run} is now shared with + </Message.Header> + <Message.List> + {usersSharedWith.map((user_email, index) => ( + <Message.Item key={index}>{user_email}</Message.Item> + ))} + </Message.List> + </Message> + )} + + {usersNotSharedWith.length > 0 && ( + <Message error> + <Message.Header> + {name} #{run} could not be shared with + </Message.Header> + <Message.List> + {usersNotSharedWith.map((failure, index) => ( + <Message.Item key={index}> + {failure.user_email_to_share_with}: {failure.error_message} + </Message.Item> + ))} + </Message.List> + </Message> + )} + + {userUnsharedWith && ( + <Message success> + {name} #{run} is no longer shared with {userUnsharedWith} + </Message> + )} + + {unshareError && ( + <Message error> + An error occurred while unsharing {name} #{run}: {unshareError} + </Message> + )} + + <Modal + open={expirationModalOpen} + onClose={closeExpirationModal} + closeIcon + size="mini" + id={styles["expiration-date-modal"]} + > + <Modal.Header>Set expiration date</Modal.Header> + <Modal.Content> + <Checkbox + label="Never expires" + checked={neverExpires} + onChange={() => { + setNeverExpires(!neverExpires); + if (!neverExpires) { + setExpirationDate(null); + } + }} + style={{ marginBottom: "1.5em" }} + /> + <SemanticDatepicker + onChange={handleChangeExpirationDate} + disabled={neverExpires} + value={expirationDate} + locale="en-US" + minDate={new Date()} + /> + </Modal.Content> + <Modal.Actions id={styles["expiration-modal-actions"]}> + <Button + primary + onClick={handleSetExpirationDate} + className="aligned left" + id={styles["expiration-modal-set-button"]} + > + Set + </Button> + </Modal.Actions> + </Modal> + + {loadingWorkflowUnshare && ( + <Loader inline id={styles["loader-unshare-workflow"]} inverted /> + )} + + <h3 id={styles["read-only-header"]}>Read-only access</h3> + + {workflowShareStatusLoading ? ( + <Loader inline id={styles["loader-share-status"]} inverted /> + ) : ( + <> + {workflowShares.length > 0 ? ( + <div> + {workflowShares.map((share, index) => ( + <div key={index} id={styles["share-item"]}> + <div> + <Icon + name="user" + size="big" + id={styles["share-user-icon"]} + /> + {share.user_email} + </div> + <div> + <Popup + content={"Expires"} + position="top center" + disabled={false} + trigger={ + <span> + <Button + size="tiny" + icon + labelPosition="left" + disabled + > + <Icon name="clock" /> + {share.valid_until + ? share.valid_until.substring(0, 10) + : "Never"} + </Button> + </span> + } + /> + + <Popup + content={"Unshare"} + position="top center" + trigger={ + <Button + size="tiny" + icon + negative + onClick={() => { + setConfirmUnshareOpen(true); + setUserToUnshareWith(share.user_email); + }} + > + <Icon name="trash" /> + </Button> + } + /> + <Confirm + size="mini" + open={confirmUnshareOpen} + header="Unshare workflow" + content={`Are you sure you want to unshare ${name} #${run} with ${userToUnshareWith}?`} + onCancel={() => setConfirmUnshareOpen(false)} + onConfirm={() => + handleUnshareWorkflow(userToUnshareWith) + } + /> + </div> + </div> + ))} + </div> + ) : ( + <Message info id={styles["no-share-message"]}> + This workflow has not been shared with anyone yet. + </Message> + )} + </> + )} + </Modal.Content> + <Modal.Actions id={styles["sharing-modal-actions"]}> + <Popup + content={"Link copied!"} + open={linkCopied} + position="top center" + trigger={ + <Button + icon + labelPosition="left" + onClick={copyCurrentUrl} + className="left floated" + id={styles["copy-link-button"]} + > + <Icon name="linkify" /> + Copy link + </Button> + } + /> + <Button onClick={onCloseModal} id={styles["done-button"]}> + Done + </Button> + </Modal.Actions> + </Modal> + ); +} diff --git a/reana-ui/src/components/WorkflowShareModal.module.scss b/reana-ui/src/components/WorkflowShareModal.module.scss new file mode 100644 index 00000000..5259bad8 --- /dev/null +++ b/reana-ui/src/components/WorkflowShareModal.module.scss @@ -0,0 +1,100 @@ +/* + -*- coding: utf-8 -*- + + This file is part of REANA. + Copyright (C) 2023 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. +*/ + +#workflow-share-modal > i { + top: 1.0535rem; + right: 1rem; + color: rgba(0, 0, 0, 0.87); +} + +#expiration-date-modal > i { + top: 1.0535rem; + right: 1rem; + color: rgba(0, 0, 0, 0.87); +} + +#loader-share-workflow:before, +#loader-share-status:before, +#loader-unshare-workflow:before { + border-color: rgba(0, 0, 0, 0.1); +} + +#loader-share-workflow:after, +#loader-share-status:after, +#loader-unshare-workflow:after { + border-color: #767676 transparent transparent; +} + +#loader-share-workflow { + margin-left: 5px; +} + +#loader-unshare-workflow { + margin-top: 1.5em; +} + +#message { + margin: 1em 0; +} + +#share-buttons { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5em; +} + +#share-button { + display: flex; + align-items: center; +} + +#expiration-date-button { + margin-right: 0; +} + +#expiration-modal-actions { + text-align: left; +} + +#expiration-modal-set-button { + margin-left: 0.5em; +} + +#read-only-header { + margin-top: 0.75em; +} + +#share-item { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 0.5em; +} + +#share-user-icon { + margin-right: 0.25em; +} + +#no-share-message { + margin-top: -0.25em; +} + +#sharing-modal-actions { + padding: 1.5em; +} + +#copy-link-button { + margin-left: 0px; +} + +#done-button { + margin-right: 0px; +} diff --git a/reana-ui/src/components/index.js b/reana-ui/src/components/index.js index 74e0fc43..323dfdaf 100644 --- a/reana-ui/src/components/index.js +++ b/reana-ui/src/components/index.js @@ -22,6 +22,7 @@ export { default as WorkflowDeleteModal } from "./WorkflowDeleteModal"; export { default as WorkflowInfo } from "./WorkflowInfo"; export { default as WorkflowBadges } from "./WorkflowBadges"; export { default as WorkflowStopModal } from "./WorkflowStopModal"; +export { default as WorkflowShareModal } from "./WorkflowShareModal"; export { default as WorkflowActionsPopup } from "./WorkflowActionsPopup"; export { default as WorkflowProgressCircleBar } from "./WorkflowProgressCircleBar"; export { default as PieChart } from "./PieChart"; diff --git a/reana-ui/src/pages/workflowDetails/WorkflowDetails.js b/reana-ui/src/pages/workflowDetails/WorkflowDetails.js index 24e9d4d7..7b6f1317 100644 --- a/reana-ui/src/pages/workflowDetails/WorkflowDetails.js +++ b/reana-ui/src/pages/workflowDetails/WorkflowDetails.js @@ -30,6 +30,7 @@ import { WorkflowActionsPopup, WorkflowBadges, WorkflowDeleteModal, + WorkflowShareModal, WorkflowStopModal, } from "~/components"; import { @@ -170,6 +171,7 @@ export default function WorkflowDetails() { <InteractiveSessionModal /> <WorkflowDeleteModal /> <WorkflowStopModal /> + <WorkflowShareModal /> </Container> </BasePage> ); diff --git a/reana-ui/src/pages/workflowList/components/WorkflowList.js b/reana-ui/src/pages/workflowList/components/WorkflowList.js index 40eca422..ec3cfad6 100644 --- a/reana-ui/src/pages/workflowList/components/WorkflowList.js +++ b/reana-ui/src/pages/workflowList/components/WorkflowList.js @@ -17,6 +17,7 @@ import { WorkflowBadges, WorkflowInfo, WorkflowDeleteModal, + WorkflowShareModal, WorkflowStopModal, WorkflowActionsPopup, InteractiveSessionModal, @@ -55,6 +56,7 @@ export default function WorkflowList({ workflows, loading }) { <InteractiveSessionModal /> <WorkflowDeleteModal /> <WorkflowStopModal /> + <WorkflowShareModal /> </> ); } diff --git a/reana-ui/src/reducers.js b/reana-ui/src/reducers.js index 0eba6c0e..49d1b6a3 100644 --- a/reana-ui/src/reducers.js +++ b/reana-ui/src/reducers.js @@ -51,6 +51,18 @@ import { USERS_YOU_SHARED_WITH_FETCH, USERS_YOU_SHARED_WITH_RECEIVED, USERS_YOU_SHARED_WITH_FETCH_ERROR, + OPEN_SHARE_WORKFLOW_MODAL, + CLOSE_SHARE_WORKFLOW_MODAL, + WORKFLOW_SHARE_STATUS_FETCH, + WORKFLOW_SHARE_STATUS_RECEIVED, + WORKFLOW_SHARE_INIT, + WORKFLOW_SHARED_SUCCESSFULLY, + WORKFLOW_SHARED_ERROR, + WORKFLOW_SHARE_FINISH, + WORKFLOW_SHARE_STATUS_FETCH_ERROR, + WORKFLOW_UNSHARE_INIT, + WORKFLOW_UNSHARED, + WORKFLOW_UNSHARE_ERROR, } from "~/actions"; import { USER_ERROR } from "./errors"; @@ -79,6 +91,7 @@ export const configInitialState = { }; const authInitialState = { + id: null, email: null, reanaToken: { value: null, @@ -99,7 +112,9 @@ const workflowsInitialState = { workflowDeleteModal: { open: false, workflow: null }, workflowStopModal: { open: false, workflow: null }, interactiveSessionModal: { open: false, workflow: null }, + workflowShareModal: { open: false, workflow: null }, workflowRefresh: null, + loadingWorkflowShareStatus: false, }; const detailsInitialState = { @@ -115,6 +130,11 @@ const quotaInitialState = { const sharingInitialState = { usersSharedWithYou: [], usersYouSharedWith: [], + loadingWorkflowShare: false, + loadingWorkflowUnshare: false, + usersWorkflowWasSharedWith: [], + usersWorkflowWasNotSharedWith: [], + userWorkflowWasUnsharedWith: null, }; const notification = (state = notificationInitialState, action) => { @@ -196,6 +216,7 @@ const auth = (state = authInitialState, action) => { case USER_RECEIVED: return { ...state, + id: action.id_, email: action.email, fullName: action.full_name, username: action.username, @@ -289,8 +310,31 @@ const workflows = (state = workflowsInitialState, action) => { ...state, interactiveSessionModal: { open: false, workflow: null }, }; + case OPEN_SHARE_WORKFLOW_MODAL: + return { + ...state, + workflowShareModal: { open: true, workflow: action.workflow }, + }; + case CLOSE_SHARE_WORKFLOW_MODAL: + return { ...state, workflowShareModal: { open: false, workflow: null } }; case WORKFLOW_LIST_REFRESH: return { ...state, workflowRefresh: Math.random() }; + case WORKFLOW_SHARE_STATUS_FETCH: + return { ...state, loadingWorkflowShareStatus: true }; + case WORKFLOW_SHARE_STATUS_RECEIVED: + return { + ...state, + workflows: { + ...state.workflows, + [action.id]: { + ...state.workflows[action.id], + sharedWith: action.workflow_share_status, + }, + }, + loadingWorkflowShareStatus: false, + }; + case WORKFLOW_SHARE_STATUS_FETCH_ERROR: + return { ...state, loadingWorkflowShareStatus: false }; default: return state; @@ -378,7 +422,7 @@ const sharing = (state = sharingInitialState, action) => { usersSharedWithYou: action.users_shared_with_you, }; case USERS_SHARED_WITH_YOU_FETCH_ERROR: - return { ...state, loading: false }; + return { ...state }; case USERS_YOU_SHARED_WITH_FETCH: return { ...state, @@ -389,7 +433,48 @@ const sharing = (state = sharingInitialState, action) => { usersYouSharedWith: action.users_you_shared_with, }; case USERS_YOU_SHARED_WITH_FETCH_ERROR: - return { ...state, loading: false }; + return { ...state }; + case WORKFLOW_SHARE_INIT: + return { + ...state, + loadingWorkflowShare: true, + usersWorkflowWasSharedWith: [], + usersWorkflowWasNotSharedWith: [], + }; + case WORKFLOW_SHARED_SUCCESSFULLY: + return { + ...state, + usersWorkflowWasSharedWith: action.users_shared_with, + }; + case WORKFLOW_SHARED_ERROR: + return { + ...state, + usersWorkflowWasNotSharedWith: action.users_not_shared_with, + }; + case WORKFLOW_SHARE_FINISH: + return { + ...state, + loadingWorkflowShare: false, + }; + case WORKFLOW_UNSHARE_INIT: + return { + ...state, + unshareError: null, + loadingWorkflowUnshare: true, + }; + case WORKFLOW_UNSHARED: + return { + ...state, + loadingWorkflowUnshare: false, + userWorkflowWasUnsharedWith: action.user_email_to_unshare_with, + }; + case WORKFLOW_UNSHARE_ERROR: + return { + ...state, + loadingWorkflowUnshare: false, + unshareError: action.error_message, + }; + default: return state; } diff --git a/reana-ui/src/selectors.js b/reana-ui/src/selectors.js index 15b87f50..c4f26a0f 100644 --- a/reana-ui/src/selectors.js +++ b/reana-ui/src/selectors.js @@ -24,6 +24,7 @@ export const getUserQuota = (state) => state.quota; // Auth export const isSignedIn = (state) => !!state.auth.email; +export const getUserId = (state) => state.auth.id; export const getUserEmail = (state) => state.auth.email; export const getUserFullName = (state) => state.auth.fullName; export const getUserFetchError = (state) => state.auth.error[USER_ERROR.fetch]; @@ -55,6 +56,16 @@ export const getInteractiveSessionModalOpen = (state) => state.workflows.interactiveSessionModal.open; export const getInteractiveSessionModalItem = (state) => state.workflows.interactiveSessionModal.workflow; +export const getWorkflowShareModalOpen = (state) => + state.workflows.workflowShareModal.open; +export const getWorkflowShareModalItem = (state) => + state.workflows.workflowShareModal.workflow; +export const getWorkflowShareStatus = (id) => (state) => + state.workflows.workflows && + state.workflows.workflows[id] && + state.workflows.workflows[id].sharedWith; +export const getLoadingWorkflowShareStatus = (state) => + state.workflows && state.workflows.loadingWorkflowShareStatus; export const getWorkflowRefresh = (state) => state.workflows.workflowRefresh; // Details @@ -77,3 +88,15 @@ export const getUsersSharedWithYou = (state) => state.sharing && state.sharing.usersSharedWithYou; export const getUsersYouSharedWith = (state) => state.sharing && state.sharing.usersYouSharedWith; +export const getLoadingWorkflowShare = (state) => + state.sharing && state.sharing.loadingWorkflowShare; +export const getLoadingWorkflowUnshare = (state) => + state.sharing && state.sharing.loadingWorkflowUnshare; +export const getUsersWorkflowWasSharedWith = (state) => + state.sharing && state.sharing.usersWorkflowWasSharedWith; +export const getUsersWorkflowWasNotSharedWith = (state) => + state.sharing && state.sharing.usersWorkflowWasNotSharedWith; +export const getUnshareError = (state) => + state.sharing && state.sharing.unshareError; +export const getUserWorkflowWasUnsharedWith = (state) => + state.sharing && state.sharing.userWorkflowWasUnsharedWith; diff --git a/reana-ui/yarn.lock b/reana-ui/yarn.lock index e48b29d1..ccaee593 100644 --- a/reana-ui/yarn.lock +++ b/reana-ui/yarn.lock @@ -18,12 +18,12 @@ integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== "@ampproject/remapping@^2.2.0": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" - integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" "@apideck/better-ajv-errors@^0.3.1": version "0.3.6" @@ -147,6 +147,17 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" +"@babel/helper-define-polyfill-provider@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz#fadc63f0c2ff3c8d02ed905dcea747c5b0fb74fd" + integrity sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + "@babel/helper-environment-visitor@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" @@ -1122,7 +1133,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.6.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== @@ -1299,6 +1310,13 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz#2cbcf822bf3764c9658c4d2e568bd0c0cb748016" integrity sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw== +"@date-fns/upgrade@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@date-fns/upgrade/-/upgrade-1.0.3.tgz#1e59d8c936f63c5c7daeeca09ec82b0849a1252e" + integrity sha512-0BLzKmXwWw3Zh3cZzW4xScmwGijXCAulaFdikqNiSnK8PAgYYSWWxOP/kuJFpKaoIT5KzstVGyHsjA7t/QXi1Q== + dependencies: + date-fns "^2.1" + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1608,32 +1626,32 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz#9b18145d26cf33d08576cf4c7665b28554480ed7" - integrity sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw== +"@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== dependencies: - "@jridgewell/set-array" "^1.0.1" + "@jridgewell/set-array" "^1.2.1" "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/trace-mapping" "^0.3.24" "@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.0.1": +"@jridgewell/set-array@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== "@jridgewell/source-map@^0.3.3": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" - integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": version "1.4.15" @@ -1648,10 +1666,10 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.23" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz#afc96847f3f07841477f303eed687707a5aacd80" - integrity sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg== +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -1766,10 +1784,10 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@remix-run/router@1.15.2": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.2.tgz#35726510d332ba5349c6398d13259d5da184553d" - integrity sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q== +"@remix-run/router@1.15.3": + version "1.15.3" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.3.tgz#d2509048d69dbb72d5389a14945339f1430b2d3c" + integrity sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w== "@rollup/plugin-babel@^5.2.0": version "5.3.1" @@ -2238,9 +2256,9 @@ "@types/node" "*" "@types/node@*": - version "20.11.23" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.23.tgz#5c156571ccb4200a2408084f472e1927d719c01e" - integrity sha512-ZUarKKfQuRILSNYt32FuPL20HS7XwNT7/uRwSV8tiHWfyyVwDLYZNF6DZKc2bove++pgfsXn9sUwII/OsQ82cQ== + version "20.11.26" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.26.tgz#3fbda536e51d5c79281e1d9657dcb0131baabd2d" + integrity sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ== dependencies: undici-types "~5.26.4" @@ -2280,16 +2298,16 @@ integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== "@types/react-dom@^18.0.0": - version "18.2.19" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.19.tgz#b84b7c30c635a6c26c6a6dfbb599b2da9788be58" - integrity sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA== + version "18.2.21" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.21.tgz#b8c81715cebdebb2994378616a8d54ace54f043a" + integrity sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw== dependencies: "@types/react" "*" "@types/react@*": - version "18.2.61" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.61.tgz#5607308495037436779939ec0348a5816c08799d" - integrity sha512-NURTN0qNnJa7O/k4XUkEW2yfygA+NxS0V5h1+kp9jPwhzZy95q3ADoGMP0+JypMhrZBTTgjKAUlTctde1zzeQA== + version "18.2.65" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.65.tgz#54eb311fa9aba173c9e163d42ec188d5a42878b8" + integrity sha512-98TsY0aW4jqx/3RqsUXwMDZSWR1Z4CUlJNue8ueS2/wcxZOsz4xmW1X8ieaWVRHcmmQM3R8xVA4XWB3dJnWwDQ== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -2490,10 +2508,10 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" - integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.11.5": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== dependencies: "@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" @@ -2508,10 +2526,10 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== -"@webassemblyjs/helper-buffer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" - integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== "@webassemblyjs/helper-numbers@1.11.6": version "1.11.6" @@ -2527,15 +2545,15 @@ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== -"@webassemblyjs/helper-wasm-section@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" - integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/ieee754@1.11.6": version "1.11.6" @@ -2557,58 +2575,58 @@ integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== "@webassemblyjs/wasm-edit@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" - integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - "@webassemblyjs/helper-wasm-section" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-opt" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" - "@webassemblyjs/wast-printer" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" -"@webassemblyjs/wasm-gen@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" - integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wasm-opt@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" - integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== dependencies: - "@webassemblyjs/ast" "1.11.6" - "@webassemblyjs/helper-buffer" "1.11.6" - "@webassemblyjs/wasm-gen" "1.11.6" - "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" -"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" - integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@webassemblyjs/helper-api-error" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/ieee754" "1.11.6" "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" -"@webassemblyjs/wast-printer@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" - integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== dependencies: - "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/ast" "1.12.1" "@xtuc/long" "4.2.2" "@xtuc/ieee754@^1.2.0": @@ -2907,6 +2925,17 @@ array.prototype.filter@^1.0.3: es-array-method-boxes-properly "^1.0.0" is-string "^1.0.7" +array.prototype.findlast@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.4.tgz#eeb9e45fc894055c82e5675c463e8077b827ad36" + integrity sha512-BMtLxpV+8BD+6ZPFIWmnUBpQoy+A+ujcg4rhp2iwCRJYA7PEh2MS4NL3lz8EiDlLrJPp2hg9qWihr5pd//jcGw== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.3.0" + es-shim-unscopables "^1.0.2" + array.prototype.findlastindex@^1.2.3: version "1.2.4" resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.4.tgz#d1c50f0b3a9da191981ff8942a0aedd82794404f" @@ -2928,7 +2957,7 @@ array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: +array.prototype.flatmap@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== @@ -2949,7 +2978,17 @@ array.prototype.reduce@^1.0.6: es-array-method-boxes-properly "^1.0.0" is-string "^1.0.7" -array.prototype.tosorted@^1.1.1: +array.prototype.toreversed@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" + integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz#c8c89348337e51b8a3c48a9227f9ce93ceedcba8" integrity sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg== @@ -3017,18 +3056,18 @@ at-least-node@^1.0.0: integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== autoprefixer@^10.4.12, autoprefixer@^10.4.13: - version "10.4.17" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.17.tgz#35cd5695cbbe82f536a50fa025d561b01fdec8be" - integrity sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg== + version "10.4.18" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.18.tgz#fcb171a3b017be7cb5d8b7a825f5aacbf2045163" + integrity sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g== dependencies: - browserslist "^4.22.2" - caniuse-lite "^1.0.30001578" + browserslist "^4.23.0" + caniuse-lite "^1.0.30001591" fraction.js "^4.3.7" normalize-range "^0.1.2" picocolors "^1.0.0" postcss-value-parser "^4.2.0" -available-typed-arrays@^1.0.6, available-typed-arrays@^1.0.7: +available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== @@ -3116,12 +3155,12 @@ babel-plugin-named-asset-import@^0.3.8: integrity sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q== babel-plugin-polyfill-corejs2@^0.4.8: - version "0.4.8" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz#dbcc3c8ca758a290d47c3c6a490d59429b0d2269" - integrity sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg== + version "0.4.10" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz#276f41710b03a64f6467433cab72cbc2653c38b1" + integrity sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ== dependencies: "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.5.0" + "@babel/helper-define-polyfill-provider" "^0.6.1" semver "^6.3.1" babel-plugin-polyfill-corejs3@^0.9.0: @@ -3286,7 +3325,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.22.2, browserslist@^4.22.3: +browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.22.2, browserslist@^4.22.3, browserslist@^4.23.0: version "4.23.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== @@ -3434,10 +3473,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001578, caniuse-lite@^1.0.30001587: - version "1.0.30001591" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz#16745e50263edc9f395895a7cd468b9f3767cf33" - integrity sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001591: + version "1.0.30001597" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz#8be94a8c1d679de23b22fbd944232aa1321639e6" + integrity sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w== canvas@^2.11.2: version "2.11.2" @@ -3528,6 +3567,11 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== +classnames@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + clean-css@^5.2.2: version "5.3.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" @@ -3961,9 +4005,9 @@ css.escape@^1.5.1: integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== cssdb@^7.1.0: - version "7.11.1" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.11.1.tgz#491841b281d337d7e5332e43b282429dd241b377" - integrity sha512-F0nEoX/Rv8ENTHsjMPGHd9opdjGfXkgRBafSUGnQKPzGZFB7Lm0BbT10x21TMOCrKLbVsJ0NoCDMk6AfKqw8/A== + version "7.11.2" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-7.11.2.tgz#127a2f5b946ee653361a5af5333ea85a39df5ae5" + integrity sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A== cssesc@^3.0.0: version "3.0.0" @@ -4078,6 +4122,26 @@ data-urls@^4.0.0: whatwg-mimetype "^3.0.0" whatwg-url "^12.0.0" +date-fns@2.29.3: + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + +date-fns@^2.0.0, date-fns@^2.1: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + +dayzed@3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/dayzed/-/dayzed-3.2.3.tgz#78c5ab7e28579cd3c0f9f848b744ac5c768e9a22" + integrity sha512-qXTIKs+R6ydWwNo+X1wu3lUptyRSGoyY+ZzRcQSM0zUlaG+/Ei+bFjqbQm1T2oJ+WKNkTHURBcGsxnx9N+9kfA== + dependencies: + "@babel/runtime" "^7.6.2" + date-fns "^2.0.0" + debug@2.6.9, debug@^2.6.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -4175,7 +4239,7 @@ default-gateway@^6.0.3: dependencies: execa "^5.0.0" -define-data-property@^1.0.1, define-data-property@^1.1.2, define-data-property@^1.1.4: +define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== @@ -4427,9 +4491,9 @@ ejs@^3.1.5, ejs@^3.1.6: jake "^10.8.5" electron-to-chromium@^1.4.668: - version "1.4.687" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.687.tgz#8b80da91848c13a90802f840c7de96c8558fef52" - integrity sha512-Ic85cOuXSP6h7KM0AIJ2hpJ98Bo4hyTUjc4yjMbkvD+8yTxEhfK9+8exT2KKYsSjnCn2tGsKVSZwE7ZgTORQCw== + version "1.4.703" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.703.tgz#786ab0c8cfe548b9da03890f923e69b1ae522741" + integrity sha512-094ZZC4nHXPKl/OwPinSMtLN9+hoFkdfQGKnvXbY+3WEAYtVDpz9UhJIViiY6Zb8agvqxiaJzNG9M+pRZWvSZw== emittery@^0.10.2: version "0.10.2" @@ -4469,9 +4533,9 @@ encoding@^0.1.12, encoding@^0.1.13: iconv-lite "^0.6.2" enhanced-resolve@^5.15.0: - version "5.15.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.1.tgz#384391e025f099e67b4b00bfd7f0906a408214e1" - integrity sha512-3d3JRbwsCLJsYgvb6NuWEG44jjPSOMuS73L/6+7BZuoKm3W+qXnSoIYVHi8dG7Qcg4inAY4jbzkZ7MnskePeDg== + version "5.16.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787" + integrity sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -4596,7 +4660,7 @@ es-get-iterator@^1.1.3: isarray "^2.0.5" stop-iteration-iterator "^1.0.0" -es-iterator-helpers@^1.0.12, es-iterator-helpers@^1.0.15: +es-iterator-helpers@^1.0.15, es-iterator-helpers@^1.0.17: version "1.0.17" resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.17.tgz#123d1315780df15b34eb181022da43e734388bb8" integrity sha512-lh7BsUqelv4KUbR5a/ZTaGGIMLCjPGPqJ6q+Oq24YP0RdyptX1uzm4vvaqzk7Zx3bpl/76YLTTDj9L7uYQ92oQ== @@ -4810,26 +4874,28 @@ eslint-plugin-react-hooks@^4.3.0: integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== eslint-plugin-react@^7.23.2, eslint-plugin-react@^7.27.1: - version "7.33.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608" - integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw== + version "7.34.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.0.tgz#ab71484d54fc409c37025c5eca00eb4177a5e88c" + integrity sha512-MeVXdReleBTdkz/bvcQMSnCXGi+c9kvy51IpinjnJgutl3YTHWsDdke7Z1ufZpGfDG8xduBDKyjtB9JH1eBKIQ== dependencies: - array-includes "^3.1.6" - array.prototype.flatmap "^1.3.1" - array.prototype.tosorted "^1.1.1" + array-includes "^3.1.7" + array.prototype.findlast "^1.2.4" + array.prototype.flatmap "^1.3.2" + array.prototype.toreversed "^1.1.2" + array.prototype.tosorted "^1.1.3" doctrine "^2.1.0" - es-iterator-helpers "^1.0.12" + es-iterator-helpers "^1.0.17" estraverse "^5.3.0" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" - object.entries "^1.1.6" - object.fromentries "^2.0.6" - object.hasown "^1.1.2" - object.values "^1.1.6" + object.entries "^1.1.7" + object.fromentries "^2.0.7" + object.hasown "^1.1.3" + object.values "^1.1.7" prop-types "^15.8.1" - resolve "^2.0.0-next.4" + resolve "^2.0.0-next.5" semver "^6.3.1" - string.prototype.matchall "^4.0.8" + string.prototype.matchall "^4.0.10" eslint-plugin-testing-library@^5.0.1: version "5.11.1" @@ -5271,6 +5337,11 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +format-string-by-pattern@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/format-string-by-pattern/-/format-string-by-pattern-1.2.2.tgz#840823f2f7c122ff8de8f0bb3f9b34a280cd5cb1" + integrity sha512-dYhtZGK/V7iif43UGjHu3ExGASwmoDdHPIejcVv/dYnCjtkzFl4XIXqKuzryj26DaXMAwvEQ3FUv09fROvgY/Q== + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -5393,7 +5464,7 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== @@ -5608,7 +5679,7 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1, has-property-descriptors@^1.0.2: +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== @@ -5625,7 +5696,7 @@ has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-tostringtag@^1.0.0, has-tostringtag@^1.0.1, has-tostringtag@^1.0.2: +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== @@ -5638,9 +5709,9 @@ has-unicode@^2.0.1: integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== hasown@^2.0.0, hasown@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.1.tgz#26f48f039de2c0f8d3356c223fb8d50253519faa" - integrity sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA== + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" @@ -5698,9 +5769,9 @@ html-encoding-sniffer@^3.0.0: whatwg-encoding "^2.0.0" html-entities@^2.1.0, html-entities@^2.3.2: - version "2.4.0" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.4.0.tgz#edd0cee70402584c8c76cc2c0556db09d1f45061" - integrity sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ== + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== html-escaper@^2.0.0: version "2.0.2" @@ -6077,10 +6148,10 @@ is-lambda@^1.0.1: resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== -is-map@^2.0.1, is-map@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" - integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== +is-map@^2.0.2, is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== is-module@^1.0.0: version "1.0.0" @@ -6154,10 +6225,10 @@ is-root@^2.1.0: resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== -is-set@^2.0.1, is-set@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" - integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== +is-set@^2.0.2, is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: version "1.0.3" @@ -6197,10 +6268,10 @@ is-typedarray@^1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== -is-weakmap@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" - integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== is-weakref@^1.0.2: version "1.0.2" @@ -6209,13 +6280,13 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" -is-weakset@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" - integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" + call-bind "^1.0.7" + get-intrinsic "^1.2.4" is-what@^3.14.1: version "3.14.1" @@ -6986,9 +7057,9 @@ jsonpointer@^5.0.0: integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== jsroot@~7.5.0: - version "7.5.4" - resolved "https://registry.yarnpkg.com/jsroot/-/jsroot-7.5.4.tgz#8756382294d629e2410f30c006b8020978bcd6af" - integrity sha512-O7qoDYwToFBEjEmElPvx8e11tSyM0IcdLMdSv1LK/Jhic2zYEsUl0t0k7IG2ieRRKIA0mmJ+1wagDyelRLlaiw== + version "7.5.5" + resolved "https://registry.yarnpkg.com/jsroot/-/jsroot-7.5.5.tgz#60a293d0a2b0b939e2f48bb33f6470876b4e5bd6" + integrity sha512-sSqXIJFm/jnNiBMiOX/k3znEwJHx6II0VcL+/Tnotyr8rxKjy/usWsbOMzFtWqGS/zQm0ny12zX5dorwpQplkA== dependencies: canvas "^2.11.2" jsdom "^22.1.0" @@ -7642,9 +7713,9 @@ mz@^2.7.0: thenify-all "^1.0.0" nan@^2.17.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" - integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w== + version "2.19.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.19.0.tgz#bb58122ad55a6c5bc973303908d5b16cfdd5a8c0" + integrity sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw== nanoid@^3.3.7: version "3.3.7" @@ -7914,7 +7985,7 @@ object.assign@^4.1.4, object.assign@^4.1.5: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.6, object.entries@^1.1.7: +object.entries@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.7.tgz#2b47760e2a2e3a752f39dd874655c61a7f03c131" integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== @@ -7923,7 +7994,7 @@ object.entries@^1.1.6, object.entries@^1.1.7: define-properties "^1.2.0" es-abstract "^1.22.1" -object.fromentries@^2.0.6, object.fromentries@^2.0.7: +object.fromentries@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== @@ -7954,7 +8025,7 @@ object.groupby@^1.0.1: es-abstract "^1.22.3" es-errors "^1.0.0" -object.hasown@^1.1.2: +object.hasown@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.3.tgz#6a5f2897bb4d3668b8e79364f98ccf971bda55ae" integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== @@ -9084,6 +9155,11 @@ react-error-overlay@^6.0.11: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== +react-fast-compare@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" + integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== + react-fast-compare@^3.0.1: version "3.2.2" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" @@ -9137,19 +9213,19 @@ react-refresh@^0.11.0: integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== react-router-dom@^6.17.0: - version "6.22.2" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.22.2.tgz#8233968a8a576f3006e5549c80f3527d2598fc9c" - integrity sha512-WgqxD2qySEIBPZ3w0sHH+PUAiamDeszls9tzqMPBDA1YYVucTBXLU7+gtRfcSnhe92A3glPnvSxK2dhNoAVOIQ== + version "6.22.3" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.22.3.tgz#9781415667fd1361a475146c5826d9f16752a691" + integrity sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw== dependencies: - "@remix-run/router" "1.15.2" - react-router "6.22.2" + "@remix-run/router" "1.15.3" + react-router "6.22.3" -react-router@6.22.2: - version "6.22.2" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.22.2.tgz#27e77e4c635a5697693b922d131d773451c98a5b" - integrity sha512-YD3Dzprzpcq+tBMHBS822tCjnWD3iIZbTeSXMY9LPSG541EfoBGyZ3bS25KEnaZjLcmQpw2AVLkFyfgXY8uvcw== +react-router@6.22.3: + version "6.22.3" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.22.3.tgz#9d9142f35e08be08c736a2082db5f0c9540a885e" + integrity sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ== dependencies: - "@remix-run/router" "1.15.2" + "@remix-run/router" "1.15.3" react-scripts@^5.0.0: version "5.0.1" @@ -9206,6 +9282,18 @@ react-scripts@^5.0.0: optionalDependencies: fsevents "^2.3.2" +react-semantic-ui-datepickers@^2.17.2: + version "2.17.2" + resolved "https://registry.yarnpkg.com/react-semantic-ui-datepickers/-/react-semantic-ui-datepickers-2.17.2.tgz#a7ddbeecfa5e3129531eb7cdf364323942a1b9a4" + integrity sha512-rYFFwdPOi3C+7vP/KpU7jgj05BoVATEOAhlEvkeQhGtMfaxUIh/9eWF7Fas5QmUsyXXGrMm8alccIwKe34m5ug== + dependencies: + "@date-fns/upgrade" "1.0.3" + classnames "2.3.2" + date-fns "2.29.3" + dayzed "3.2.3" + format-string-by-pattern "1.2.2" + react-fast-compare "3.2.0" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -9458,7 +9546,7 @@ resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^2.0.0-next.4: +resolve@^2.0.0-next.5: version "2.0.0-next.5" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== @@ -9526,12 +9614,12 @@ run-parallel@^1.1.9: queue-microtask "^1.2.2" safe-array-concat@^1.0.0, safe-array-concat@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.0.tgz#8d0cae9cb806d6d1c06e08ab13d847293ebe0692" - integrity sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg== + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== dependencies: - call-bind "^1.0.5" - get-intrinsic "^1.2.2" + call-bind "^1.0.7" + get-intrinsic "^1.2.4" has-symbols "^1.0.3" isarray "^2.0.5" @@ -9776,16 +9864,16 @@ set-blocking@^2.0.0: integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== set-function-length@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.1.tgz#47cc5945f2c771e2cf261c6737cf9684a2a5e425" - integrity sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g== + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: - define-data-property "^1.1.2" + define-data-property "^1.1.4" es-errors "^1.3.0" function-bind "^1.1.2" - get-intrinsic "^1.2.3" + get-intrinsic "^1.2.4" gopd "^1.0.1" - has-property-descriptors "^1.0.1" + has-property-descriptors "^1.0.2" set-function-name@^2.0.0, set-function-name@^2.0.1: version "2.0.2" @@ -9849,11 +9937,11 @@ shell-quote@^1.6.1, shell-quote@^1.7.3, shell-quote@^1.8.1: integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== side-channel@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.5.tgz#9a84546599b48909fb6af1211708d23b1946221b" - integrity sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ== + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: - call-bind "^1.0.6" + call-bind "^1.0.7" es-errors "^1.3.0" get-intrinsic "^1.2.4" object-inspect "^1.13.1" @@ -10169,7 +10257,7 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string.prototype.matchall@^4.0.6, string.prototype.matchall@^4.0.8: +string.prototype.matchall@^4.0.10, string.prototype.matchall@^4.0.6: version "4.0.10" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== @@ -10492,9 +10580,9 @@ terser-webpack-plugin@^5.2.5, terser-webpack-plugin@^5.3.10: terser "^5.26.0" terser@^5.0.0, terser@^5.10.0, terser@^5.26.0: - version "5.28.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.28.1.tgz#bf00f7537fd3a798c352c2d67d67d65c915d1b28" - integrity sha512-wM+bZp54v/E9eRRGXb5ZFDvinrJIOaTapx3WUokyVGZu5ucVCK55zEgGd5Dl2fSr3jUo5sDiERErUWLY6QPFyA== + version "5.29.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.29.1.tgz#44e58045b70c09792ba14bfb7b4e14ca8755b9fa" + integrity sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -11266,25 +11354,25 @@ which-builtin-type@^1.1.3: which-typed-array "^1.1.9" which-collection@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" - integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== dependencies: - is-map "^2.0.1" - is-set "^2.0.1" - is-weakmap "^2.0.1" - is-weakset "^2.0.1" + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.9: - version "1.1.14" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.14.tgz#1f78a111aee1e131ca66164d8bdc3ab062c95a06" - integrity sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg== + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== dependencies: - available-typed-arrays "^1.0.6" - call-bind "^1.0.5" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" for-each "^0.3.3" gopd "^1.0.1" - has-tostringtag "^1.0.1" + has-tostringtag "^1.0.2" which@^1.2.9, which@^1.3.1: version "1.3.1" @@ -11570,9 +11658,9 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== yaml@^2.3.4: - version "2.4.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.0.tgz#2376db1083d157f4b3a452995803dbcf43b08140" - integrity sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ== + version "2.4.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed" + integrity sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg== yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" From 089853fc2a3347baf0fc4d260d3915293f9913c1 Mon Sep 17 00:00:00 2001 From: Daan Rosendal <daan.rosendal@cern.ch> Date: Fri, 15 Dec 2023 12:09:41 +0100 Subject: [PATCH 3/4] feat(ui): make details available for shared workflows (#375) Closes reanahub/reana-server#651 --- reana-ui/src/actions.js | 10 ++- reana-ui/src/client.js | 4 +- .../src/components/WorkflowActionsPopup.js | 18 ++++- reana-ui/src/components/WorkflowBadges.js | 78 ++++++++++++------- 4 files changed, 75 insertions(+), 35 deletions(-) diff --git a/reana-ui/src/actions.js b/reana-ui/src/actions.js index ad25e5f2..14af4e62 100644 --- a/reana-ui/src/actions.js +++ b/reana-ui/src/actions.js @@ -293,11 +293,13 @@ export function fetchWorkflows({ sort, showLoader = true, workflowIdOrName, + includeShared = false, }) { return async (dispatch) => { if (showLoader) { dispatch({ type: WORKFLOWS_FETCH }); } + return await client .getWorkflows({ pagination, @@ -307,15 +309,16 @@ export function fetchWorkflows({ sharedWith, sort, workflowIdOrName, + includeShared, }) - .then((resp) => + .then((resp) => { dispatch({ type: WORKFLOWS_RECEIVED, workflows: parseWorkflows(resp.data.items), total: resp.data.total, userHasWorkflows: resp.data.user_has_workflows, - }), - ) + }); + }) .catch((err) => { dispatch(errorActionCreator(err, USER_INFO_URL)); dispatch({ type: WORKFLOWS_FETCH_ERROR }); @@ -335,6 +338,7 @@ export function fetchWorkflow(id, { refetch = false, showLoader = true } = {}) { fetchWorkflows({ workflowIdOrName: id, showLoader, + includeShared: true, }), ); } diff --git a/reana-ui/src/client.js b/reana-ui/src/client.js index 9d508679..f7f42e2d 100644 --- a/reana-ui/src/client.js +++ b/reana-ui/src/client.js @@ -131,14 +131,16 @@ class Client { sharedWith, sort, workflowIdOrName, + includeShared = false, } = {}) { let shared = false; - if (ownedBy === "anybody") { + if (ownedBy === "anybody" || includeShared) { ownedBy = undefined; shared = true; } else if (ownedBy === "you") { ownedBy = undefined; } + return this._request( WORKFLOWS_URL({ ...(pagination ?? {}), diff --git a/reana-ui/src/components/WorkflowActionsPopup.js b/reana-ui/src/components/WorkflowActionsPopup.js index 3ecf5bc5..d0d551ea 100644 --- a/reana-ui/src/components/WorkflowActionsPopup.js +++ b/reana-ui/src/components/WorkflowActionsPopup.js @@ -29,7 +29,11 @@ import styles from "./WorkflowActionsPopup.module.scss"; const JupyterIcon = <JupyterNotebookIcon className={styles["jupyter-icon"]} />; -export default function WorkflowActionsPopup({ workflow, className }) { +export default function WorkflowActionsPopup({ + workflow, + className, + insideClickableElement, +}) { const dispatch = useDispatch(); const [open, setOpen] = useState(false); const { id, size, status, session_status: sessionStatus } = workflow; @@ -111,6 +115,17 @@ export default function WorkflowActionsPopup({ workflow, className }) { }); } + if (workflow.owner_email !== "-") { + return ( + <div + className={className || styles.container} + style={ + insideClickableElement ? { cursor: "pointer" } : { cursor: "default" } + } + /> + ); + } + return ( <div className={className}> {menuItems.length > 0 && ( @@ -145,4 +160,5 @@ WorkflowActionsPopup.defaultProps = { WorkflowActionsPopup.propTypes = { workflow: workflowShape.isRequired, className: PropTypes.string, + insideClickableElement: PropTypes.bool, }; diff --git a/reana-ui/src/components/WorkflowBadges.js b/reana-ui/src/components/WorkflowBadges.js index 5bf13a46..113eb26a 100644 --- a/reana-ui/src/components/WorkflowBadges.js +++ b/reana-ui/src/components/WorkflowBadges.js @@ -9,7 +9,7 @@ */ import styles from "./WorkflowBadges.module.scss"; -import { Label } from "semantic-ui-react"; +import { Icon, Label, Popup } from "semantic-ui-react"; import { JupyterNotebookIcon } from "~/components"; import { INTERACTIVE_SESSION_URL } from "~/client"; import { LauncherLabel } from "~/components"; @@ -29,36 +29,54 @@ export default function WorkflowBadges({ workflow }) { return ( <div className={styles.badgesContainer}> - {workflow.duration && ( - <Label - basic - size="tiny" - content={`CPU ${workflow.duration}`} - icon="clock" - /> - )} - {hasDiskUsage && ( - <Label - basic - size="tiny" - content={`Disk ${size.human_readable}`} - icon="hdd" - /> - )} - <LauncherLabel url={launcherURL} /> - {isSessionOpen && ( - <Label - size="tiny" - content={"Notebook"} - icon={ - <i className="icon"> - <JupyterNotebookIcon size={12} /> - </i> + {workflow.owner_email === "-" ? ( + <> + {workflow.duration && ( + <Label + basic + size="tiny" + content={`CPU ${workflow.duration}`} + icon="clock" + /> + )} + {hasDiskUsage && ( + <Label + basic + size="tiny" + content={`Disk ${size.human_readable}`} + icon="hdd" + /> + )} + <LauncherLabel url={launcherURL} /> + {isSessionOpen && ( + <Label + size="tiny" + content={"Notebook"} + icon={ + <i className="icon"> + <JupyterNotebookIcon size={12} /> + </i> + } + as="a" + href={INTERACTIVE_SESSION_URL(sessionUri, reanaToken)} + target="_blank" + rel="noopener noreferrer" + /> + )} + </> + ) : ( + <Popup + trigger={ + <span className={styles.owner}> + <Icon name="eye" style={{ marginTop: "-3px" }} /> + {workflow.owner_email} + </span> + } + position="top center" + content={ + "This workflow is read-only shared with you by " + + workflow.owner_email } - as="a" - href={INTERACTIVE_SESSION_URL(sessionUri, reanaToken)} - target="_blank" - rel="noopener noreferrer" /> )} </div> From 83eee025b560c97467325747c2a95319ec6bd04e Mon Sep 17 00:00:00 2001 From: Marco Donadoni <marco.donadoni@cern.ch> Date: Mon, 26 Aug 2024 12:01:57 +0200 Subject: [PATCH 4/4] fix(ui): fix workflow sharing interface after UI changes (#375) --- reana-ui/src/actions.js | 143 ++--- reana-ui/src/client.js | 25 +- .../src/components/WorkflowActionsPopup.js | 33 +- reana-ui/src/components/WorkflowBadges.js | 17 +- reana-ui/src/components/WorkflowShareModal.js | 552 +++++++++--------- .../components/WorkflowShareModal.module.scss | 81 +-- .../src/pages/workflowList/WorkflowList.js | 18 +- .../workflowList/WorkflowList.module.scss | 35 +- .../components/WorkflowSharingFilter.js | 70 +-- .../WorkflowSharingFilter.module.scss | 43 +- reana-ui/src/reducers.js | 92 +-- reana-ui/src/selectors.js | 13 +- reana-ui/src/util.js | 1 + 13 files changed, 441 insertions(+), 682 deletions(-) diff --git a/reana-ui/src/actions.js b/reana-ui/src/actions.js index 14af4e62..33e21121 100644 --- a/reana-ui/src/actions.js +++ b/reana-ui/src/actions.js @@ -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"; @@ -96,26 +99,11 @@ 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 WORKFLOW_SHARE_INIT = "Initialise workflow sharing"; -export const WORKFLOW_SHARED_SUCCESSFULLY = "Workflow shared successfully"; -export const WORKFLOW_SHARED_ERROR = "Workflow shared error"; -export const WORKFLOW_SHARE_FINISH = "Finish workflow sharing"; -export const WORKFLOW_UNSHARE_INIT = "Initialise workflow unsharing"; -export const WORKFLOW_UNSHARED = "Workflow unshared"; -export const WORKFLOW_UNSHARE_ERROR = "Workflow unshare error"; - -export const USERS_SHARED_WITH_YOU_FETCH = - "Fetch users who shared workflows with you"; + export const USERS_SHARED_WITH_YOU_RECEIVED = "Users who shared workflows with you received"; -export const USERS_SHARED_WITH_YOU_FETCH_ERROR = - "Fetch users who shared workflows with you error"; -export const USERS_YOU_SHARED_WITH_FETCH = - "Fetch users you shared workflows with"; export const USERS_YOU_SHARED_WITH_RECEIVED = "Users you shared workflows with received"; -export const USERS_YOU_SHARED_WITH_FETCH_ERROR = - "Fetch users you shared workflows with error"; export function errorActionCreator(error, name) { const { status, data } = error?.response; @@ -288,12 +276,12 @@ export function fetchWorkflows({ pagination, search, status, - ownedBy, + sharedBy, sharedWith, sort, showLoader = true, workflowIdOrName, - includeShared = false, + shared = false, }) { return async (dispatch) => { if (showLoader) { @@ -305,20 +293,20 @@ export function fetchWorkflows({ pagination, search: formatSearch(search), status, - ownedBy, + sharedBy, sharedWith, sort, workflowIdOrName, - includeShared, + shared, }) - .then((resp) => { + .then((resp) => dispatch({ type: WORKFLOWS_RECEIVED, workflows: parseWorkflows(resp.data.items), total: resp.data.total, userHasWorkflows: resp.data.user_has_workflows, - }); - }) + }), + ) .catch((err) => { dispatch(errorActionCreator(err, USER_INFO_URL)); dispatch({ type: WORKFLOWS_FETCH_ERROR }); @@ -338,7 +326,7 @@ export function fetchWorkflow(id, { refetch = false, showLoader = true } = {}) { fetchWorkflows({ workflowIdOrName: id, showLoader, - includeShared: true, + shared: true, }), ); } @@ -567,38 +555,34 @@ export function closeShareWorkflowModal() { export function fetchUsersSharedWithYou() { return async (dispatch) => { - dispatch({ type: USERS_SHARED_WITH_YOU_FETCH }); - return await client .getUsersSharedWithYou() .then((resp) => { dispatch({ type: USERS_SHARED_WITH_YOU_RECEIVED, - users_shared_with_you: resp.data.users_shared_with_you, + usersSharedYouWith: resp.data.users_shared_with_you, }); return resp; }) .catch((err) => { - dispatch(errorActionCreator(err, USERS_SHARED_WITH_YOU_FETCH_ERROR)); + dispatch(errorActionCreator(err, USERS_SHARED_WITH_YOU_URL)); }); }; } export function fetchUsersYouSharedWith() { return async (dispatch) => { - dispatch({ type: USERS_YOU_SHARED_WITH_FETCH }); - return await client .getUsersYouSharedWith() .then((resp) => { dispatch({ type: USERS_YOU_SHARED_WITH_RECEIVED, - users_you_shared_with: resp.data.users_you_shared_with, + usersYouSharedWith: resp.data.users_you_shared_with, }); return resp; }) .catch((err) => { - dispatch(errorActionCreator(err, USERS_YOU_SHARED_WITH_FETCH_ERROR)); + dispatch(errorActionCreator(err, USERS_YOU_SHARED_WITH_URL)); }); }; } @@ -609,83 +593,24 @@ export function fetchWorkflowShareStatus(id) { 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, - workflow_share_status: resp.data.shared_with, + sharedWith, }); return resp; }) .catch((err) => { - dispatch(errorActionCreator(err, WORKFLOW_SHARE_STATUS_FETCH_ERROR)); - }); - }; -} - -export function shareWorkflow( - id, - user_id, - user_emails_to_share_with, - valid_until, -) { - return async (dispatch) => { - dispatch({ type: WORKFLOW_SHARE_INIT }); - - const users_shared_with = []; - const users_not_shared_with = []; - - for (const user_email_to_share_with of user_emails_to_share_with) { - await client - .shareWorkflow(id, { - user_id, - user_email_to_share_with, - valid_until, - }) - .then(() => { - users_shared_with.push(user_email_to_share_with); - }) - .catch((err) => { - const error_message = err.response.data.message; - users_not_shared_with.push({ - user_email_to_share_with, - error_message, - }); - }); - - if (users_shared_with.length > 0) { - dispatch({ - type: WORKFLOW_SHARED_SUCCESSFULLY, - users_shared_with, - }); - } - - if (users_not_shared_with.length > 0) { - dispatch({ - type: WORKFLOW_SHARED_ERROR, - users_not_shared_with, - }); - } - } - - dispatch({ type: WORKFLOW_SHARE_FINISH }); - }; -} - -export function unshareWorkflow(id, user_id, user_email_to_unshare_with) { - return async (dispatch) => { - dispatch({ type: WORKFLOW_UNSHARE_INIT }); - - return await client - .unshareWorkflow(id, { - user_id, - user_email_to_unshare_with, - }) - .then(() => { - dispatch({ type: WORKFLOW_UNSHARED, user_email_to_unshare_with }); - }) - .catch((err) => { - const error_message = err.response.data.message; - dispatch({ type: WORKFLOW_UNSHARE_ERROR, error_message }); + dispatch({ type: WORKFLOW_SHARE_STATUS_FETCH_ERROR }); + dispatch(errorActionCreator(err, WORKFLOW_SHARE_STATUS_URL(id))); }); }; } diff --git a/reana-ui/src/client.js b/reana-ui/src/client.js index f7f42e2d..b9a20e20 100644 --- a/reana-ui/src/client.js +++ b/reana-ui/src/client.js @@ -127,20 +127,12 @@ class Client { pagination, search, status, - ownedBy, + sharedBy, sharedWith, sort, workflowIdOrName, - includeShared = false, + shared, } = {}) { - let shared = false; - if (ownedBy === "anybody" || includeShared) { - ownedBy = undefined; - shared = true; - } else if (ownedBy === "you") { - ownedBy = undefined; - } - return this._request( WORKFLOWS_URL({ ...(pagination ?? {}), @@ -148,7 +140,7 @@ class Client { search, status, shared, - shared_by: ownedBy, + shared_by: sharedBy, shared_with: sharedWith, sort, }), @@ -238,16 +230,19 @@ class Client { return this._request(WORKFLOW_SHARE_STATUS_URL(id)); } - shareWorkflow(id, data) { + shareWorkflow(id, { userEmailToShareWith, validUntil }) { return this._request(WORKFLOW_SHARE_URL(id), { - data, + data: { + user_email_to_share_with: userEmailToShareWith, + valid_until: validUntil, + }, method: "post", }); } - unshareWorkflow(id, data) { + unshareWorkflow(id, { userEmailToUnshareWith }) { return this._request(WORKFLOW_UNSHARE_URL(id), { - data, + data: { user_email_to_unshare_with: userEmailToUnshareWith }, method: "post", }); } diff --git a/reana-ui/src/components/WorkflowActionsPopup.js b/reana-ui/src/components/WorkflowActionsPopup.js index d0d551ea..25b6252c 100644 --- a/reana-ui/src/components/WorkflowActionsPopup.js +++ b/reana-ui/src/components/WorkflowActionsPopup.js @@ -8,20 +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"; @@ -29,13 +30,10 @@ import styles from "./WorkflowActionsPopup.module.scss"; const JupyterIcon = <JupyterNotebookIcon className={styles["jupyter-icon"]} />; -export default function WorkflowActionsPopup({ - workflow, - className, - insideClickableElement, -}) { +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; @@ -63,7 +61,6 @@ export default function WorkflowActionsPopup({ onClick: (e) => { dispatch(openShareWorkflowModal(workflow)); setOpen(false); - e.stopPropagation(); }, }); @@ -115,20 +112,9 @@ export default function WorkflowActionsPopup({ }); } - if (workflow.owner_email !== "-") { - return ( - <div - className={className || styles.container} - style={ - insideClickableElement ? { cursor: "pointer" } : { cursor: "default" } - } - /> - ); - } - return ( <div className={className}> - {menuItems.length > 0 && ( + {workflow.ownerEmail === userEmail && menuItems.length > 0 && ( <Popup basic trigger={ @@ -160,5 +146,4 @@ WorkflowActionsPopup.defaultProps = { WorkflowActionsPopup.propTypes = { workflow: workflowShape.isRequired, className: PropTypes.string, - insideClickableElement: PropTypes.bool, }; diff --git a/reana-ui/src/components/WorkflowBadges.js b/reana-ui/src/components/WorkflowBadges.js index 113eb26a..c1652c40 100644 --- a/reana-ui/src/components/WorkflowBadges.js +++ b/reana-ui/src/components/WorkflowBadges.js @@ -9,15 +9,16 @@ */ import styles from "./WorkflowBadges.module.scss"; -import { Icon, Label, Popup } from "semantic-ui-react"; +import { Label, Popup } from "semantic-ui-react"; import { JupyterNotebookIcon } from "~/components"; import { INTERACTIVE_SESSION_URL } from "~/client"; import { LauncherLabel } from "~/components"; -import { getReanaToken } from "~/selectors"; +import { getReanaToken, getUserEmail } from "~/selectors"; import { useSelector } from "react-redux"; export default function WorkflowBadges({ workflow }) { const reanaToken = useSelector(getReanaToken); + const userEmail = useSelector(getUserEmail); const { size, launcherURL, @@ -29,7 +30,7 @@ export default function WorkflowBadges({ workflow }) { return ( <div className={styles.badgesContainer}> - {workflow.owner_email === "-" ? ( + {workflow.ownerEmail === userEmail && ( <> {workflow.duration && ( <Label @@ -64,18 +65,16 @@ export default function WorkflowBadges({ workflow }) { /> )} </> - ) : ( + )} + {workflow.ownerEmail !== userEmail && ( <Popup trigger={ - <span className={styles.owner}> - <Icon name="eye" style={{ marginTop: "-3px" }} /> - {workflow.owner_email} - </span> + <Label basic size="tiny" content={workflow.ownerEmail} icon="eye" /> } position="top center" content={ "This workflow is read-only shared with you by " + - workflow.owner_email + workflow.ownerEmail } /> )} diff --git a/reana-ui/src/components/WorkflowShareModal.js b/reana-ui/src/components/WorkflowShareModal.js index 8bdc7562..afca1e63 100644 --- a/reana-ui/src/components/WorkflowShareModal.js +++ b/reana-ui/src/components/WorkflowShareModal.js @@ -8,88 +8,46 @@ under the terms of the MIT License; see LICENSE file for more details. */ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import SemanticDatepicker from "react-semantic-ui-datepickers"; import { Button, - Modal, - Icon, - Popup, Checkbox, - Dropdown, - TextArea, + Confirm, + Form, + Header, + Icon, Loader, Message, - Confirm, + Modal, + Popup, } from "semantic-ui-react"; +import { closeShareWorkflowModal, fetchWorkflowShareStatus } from "~/actions"; +import client from "~/client"; import { - closeShareWorkflowModal, - fetchWorkflowShareStatus, - shareWorkflow, - unshareWorkflow, -} from "~/actions"; -import { - getLoadingWorkflowShare, getLoadingWorkflowShareStatus, - getLoadingWorkflowUnshare, - getUnshareError, - getUserId, - getUserWorkflowWasUnsharedWith, - getUsersWorkflowWasNotSharedWith, - getUsersWorkflowWasSharedWith, getWorkflowShareModalItem, getWorkflowShareModalOpen, getWorkflowShareStatus, } from "~/selectors"; import styles from "./WorkflowShareModal.module.scss"; -import SemanticDatepicker from "react-semantic-ui-datepickers"; -export default function WorkflowShareModal() { +function WorkflowShareStatus({ + workflow, + handleUnshareWorkflowSuccess, + handleUnshareWorkflowError, +}) { const dispatch = useDispatch(); - const open = useSelector(getWorkflowShareModalOpen); - const workflow = useSelector(getWorkflowShareModalItem); - const [linkCopied, setLinkCopied] = useState(false); - const [dropDownOptions, setDropDownOptions] = useState([]); - const [usersToShareWith, setUsersToShareWith] = useState([]); - const [expirationModalOpen, setExpirationModalOpen] = useState(false); - const [expirationDate, setExpirationDate] = useState(null); - const [neverExpires, setNeverExpires] = useState(true); - const [workflowShares, setWorkflowShares] = useState([]); - const workflowShareStatus = useSelector(getWorkflowShareStatus(workflow?.id)); - const [workflowShareStatusLoading, setWorkflowShareStatusLoading] = - useState(false); const loadingWorkflowShareStatus = useSelector(getLoadingWorkflowShareStatus); + const workflowShareStatus = useSelector(getWorkflowShareStatus(workflow?.id)); - const loadingWorkflowShare = useSelector(getLoadingWorkflowShare); - const loadingWorkflowUnshare = useSelector(getLoadingWorkflowUnshare); - const [unshareError, setUnshareError] = useState(null); - const workflowUnshareError = useSelector(getUnshareError); - const [usersSharedWith, setUsersSharedWith] = useState([]); - const usersWorkflowWasSharedWith = useSelector(getUsersWorkflowWasSharedWith); - const [usersNotSharedWith, setUsersNotSharedWith] = useState([]); - const usersWorkflowWasNotSharedWith = useSelector( - getUsersWorkflowWasNotSharedWith, - ); + const [loadingUnshareWorkflow, setLoadingUnshareWorkflow] = useState(false); const [confirmUnshareOpen, setConfirmUnshareOpen] = useState(false); const [userToUnshareWith, setUserToUnshareWith] = useState(null); - const [userUnsharedWith, setUserUnsharedWith] = useState(null); - const userWorkflowWasUnsharedWith = useSelector( - getUserWorkflowWasUnsharedWith, - ); - const userId = useSelector(getUserId); - - const resetMessages = () => { - setUsersSharedWith([]); - setUsersNotSharedWith([]); - setUserUnsharedWith(null); - setUnshareError(null); - }; - - useEffect(() => { - resetMessages(); - }, [open]); + const { name, run, id } = workflow; useEffect(() => { if (workflow) { @@ -97,58 +55,157 @@ export default function WorkflowShareModal() { } }, [dispatch, workflow]); - useEffect(() => { - if (!workflowShareStatus) return; - setWorkflowShares(workflowShareStatus); - }, [workflowShareStatus]); + const handleUnshareWorkflow = (userEmailToUnshareWith) => { + setLoadingUnshareWorkflow(true); + client + .unshareWorkflow(id, { + userEmailToUnshareWith, + }) + .then(() => { + dispatch(fetchWorkflowShareStatus(id)); + handleUnshareWorkflowSuccess(userEmailToUnshareWith); + setLoadingUnshareWorkflow(false); + setConfirmUnshareOpen(false); + }) + .catch((err) => { + const errorMessage = err.response.data.message; + handleUnshareWorkflowError(errorMessage); + setLoadingUnshareWorkflow(false); + setConfirmUnshareOpen(false); + }); + }; - useEffect(() => { - setWorkflowShareStatusLoading(loadingWorkflowShareStatus); - }, [loadingWorkflowShareStatus]); + return ( + <div> + <Header as="h3">Read-only access</Header> + {loadingWorkflowShareStatus ? ( + <Loader inline className={styles["loader-share-status"]} inverted /> + ) : ( + <> + {workflowShareStatus?.length > 0 ? ( + <div> + {workflowShareStatus.map((share, index) => ( + <div key={index} className={styles["share-item"]}> + <div> + <Icon + name="user" + size="big" + className={styles["share-user-icon"]} + /> + {share.userEmail} + </div> + <div> + <Popup + content={"Expires"} + position="top center" + disabled={false} + trigger={ + <span> + <Button + size="tiny" + icon + labelPosition="left" + disabled + > + <Icon name="clock" /> + {share.validUntil + ? share.validUntil.substring(0, 10) + : "Never"} + </Button> + </span> + } + /> + + <Popup + content="Unshare" + position="top center" + trigger={ + <Button + size="tiny" + icon + negative + onClick={() => { + setUserToUnshareWith(share.userEmail); + setConfirmUnshareOpen(true); + }} + disabled={loadingUnshareWorkflow} + loading={loadingUnshareWorkflow} + > + <Icon name="trash" /> + </Button> + } + /> + </div> + </div> + ))} + <Confirm + size="mini" + open={confirmUnshareOpen} + header="Unshare workflow" + content={`Are you sure you want to unshare ${name} #${run} with ${userToUnshareWith}?`} + onCancel={() => { + setConfirmUnshareOpen(false); + setUserToUnshareWith(null); + }} + onConfirm={() => handleUnshareWorkflow(userToUnshareWith)} + /> + </div> + ) : ( + <Message info> + This workflow has not been shared with anyone yet. + </Message> + )} + </> + )} + </div> + ); +} - useEffect(() => { - if (!loadingWorkflowShare) { - setUsersSharedWith(usersWorkflowWasSharedWith); - setUsersNotSharedWith(usersWorkflowWasNotSharedWith); - setUserUnsharedWith(null); - setUsersToShareWith([]); - if (workflow) dispatch(fetchWorkflowShareStatus(workflow.id)); - } - // disable eslint because we want to run this effect only when - // loadingWorkflowShare changes, for further context: - // https://github.com/facebook/create-react-app/issues/6880#issuecomment-485963251 - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loadingWorkflowShare]); +export default function WorkflowShareModal() { + const dispatch = useDispatch(); + const open = useSelector(getWorkflowShareModalOpen); + const workflow = useSelector(getWorkflowShareModalItem); + const [linkCopied, setLinkCopied] = useState(false); + const [dropDownOptions, setDropDownOptions] = useState([]); + const [usersToShareWith, setUsersToShareWith] = useState([]); + const [message, setMessage] = useState(""); + const [expirationModalOpen, setExpirationModalOpen] = useState(false); + const [expirationDate, setExpirationDate] = useState(null); + const [neverExpires, setNeverExpires] = useState(true); - useEffect(() => { - if (!loadingWorkflowUnshare) { - if (workflow) dispatch(fetchWorkflowShareStatus(workflow.id)); + const [loadingShareWorkflow, setLoadingShareWorkflow] = useState(false); + const [lastSharingAction, setLastSharingAction] = useState({}); - if (!workflowUnshareError) { - setUserUnsharedWith(userWorkflowWasUnsharedWith); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loadingWorkflowUnshare]); + const resetShareForm = useCallback(() => { + setUsersToShareWith([]); + setMessage(""); + setExpirationModalOpen(false); + setExpirationDate(null); + setNeverExpires(true); + }, []); + + const resetState = useCallback(() => { + resetShareForm(); + setLinkCopied(false); + setDropDownOptions([]); + setLoadingShareWorkflow(false); + setLastSharingAction({}); + }, [resetShareForm]); useEffect(() => { - setUnshareError(workflowUnshareError); - if (workflowUnshareError) { - setUsersSharedWith([]); - setUsersNotSharedWith([]); - if (workflow) dispatch(fetchWorkflowShareStatus(workflow.id)); - } - }, [workflowUnshareError, dispatch, workflow]); + resetState(); + }, [workflow, open, resetState]); if (!workflow) return null; const onCloseModal = () => { dispatch(closeShareWorkflowModal()); - resetMessages(); + resetState(); }; const copyCurrentUrl = () => { - navigator.clipboard.writeText(window.location.href); + const detailsURL = new URL(`/details/${workflow.id}`, window.location.href); + navigator.clipboard.writeText(detailsURL.toString()); setLinkCopied(true); setTimeout(() => { @@ -187,68 +244,89 @@ export default function WorkflowShareModal() { setExpirationModalOpen(false); }; - const handleShareWorkflow = () => { - resetMessages(); + const handleShareWorkflow = (usersToShareWith) => { if (usersToShareWith.length === 0) { return; } - dispatch( - shareWorkflow( - workflow.id, - userId, - usersToShareWith, - expirationDate?.toLocaleDateString("sv-SE", { - year: "numeric", - month: "2-digit", - day: "2-digit", - }), - ), - ); - }; + const id = workflow.id; + const validUntil = expirationDate?.toLocaleDateString("sv-SE", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + + const usersSharedWith = []; + const usersNotSharedWith = []; + const requests = []; + + setLoadingShareWorkflow(true); + + for (const userEmailToShareWith of usersToShareWith) { + const req = client + .shareWorkflow(id, { + userEmailToShareWith, + validUntil, + }) + .then(() => { + usersSharedWith.push(userEmailToShareWith); + }) + .catch((err) => { + const errorMessage = err.response.data.message; + usersNotSharedWith.push({ + userEmailToShareWith, + errorMessage, + }); + }); + + requests.push(req); + } - const handleUnshareWorkflow = (userToUnshareWith) => { - setConfirmUnshareOpen(false); - setUsersSharedWith([]); - setUsersNotSharedWith([]); - setUserUnsharedWith(null); - dispatch(unshareWorkflow(workflow.id, userId, userToUnshareWith)); + Promise.allSettled(requests).then(() => { + // update share status + dispatch(fetchWorkflowShareStatus(id)); + // reset form and share button + resetShareForm(); + setLoadingShareWorkflow(false); + // set results of last sharing action + setLastSharingAction({ + usersSharedWith, + usersNotSharedWith, + }); + }); }; const { name, run } = workflow; return ( - <Modal - open={open} - onClose={onCloseModal} - closeIcon - size="tiny" - id={styles["workflow-share-modal"]} - > + <Modal open={open} onClose={onCloseModal} closeIcon size="tiny"> <Modal.Header> Share {name} #{run} </Modal.Header> <Modal.Content> - <Dropdown - options={dropDownOptions} - placeholder="Emails of users to share with" - search - selection - fluid - allowAdditions - multiple - noResultsMessage={null} - value={usersToShareWith} - onAddItem={handleAddition} - onChange={handleChange} - disabled={loadingWorkflowShare} - /> + <Form> + <Form.Dropdown + options={dropDownOptions} + placeholder="Emails of users to share with" + search + selection + fluid + allowAdditions + multiple + noResultsMessage={null} + value={usersToShareWith} + onAddItem={handleAddition} + onChange={handleChange} + disabled={loadingShareWorkflow} + /> - <div className="ui form" id={styles["message"]}> - <TextArea placeholder="Message (optional)" /> - </div> - <div id={styles["share-buttons"]}> - <div id={styles["share-button"]}> + <Form.TextArea + placeholder="Message (optional)" + disabled={loadingShareWorkflow} + value={message} + onChange={(_, { value }) => setMessage(value)} + /> + <div className={styles["share-buttons"]}> <Popup content={ "Please provide at least one user to share the workflow with." @@ -261,10 +339,11 @@ export default function WorkflowShareModal() { primary icon labelPosition="left" - onClick={() => { - handleShareWorkflow(); - }} - disabled={usersToShareWith.length === 0} + onClick={() => handleShareWorkflow(usersToShareWith)} + disabled={ + usersToShareWith.length === 0 || loadingShareWorkflow + } + loading={loadingShareWorkflow} > <Icon name="share alternate" /> Share @@ -272,71 +351,68 @@ export default function WorkflowShareModal() { </span> } /> - - {loadingWorkflowShare && ( - <Loader inline id={styles["loader-share-workflow"]} inverted /> - )} + <Popup + content={"Expires"} + position="top center" + trigger={ + <Button + onClick={openExpirationModal} + icon + labelPosition="left" + className={styles["expiration-date-button"]} + > + <Icon name="clock" /> + {neverExpires || !expirationDate + ? "Never" + : expirationDate?.toLocaleDateString("sv-SE", { + year: "numeric", + month: "2-digit", + day: "2-digit", + })} + </Button> + } + /> </div> - <Popup - content={"Expires"} - position="top center" - trigger={ - <Button - onClick={openExpirationModal} - icon - labelPosition="left" - id={styles["expiration-date-button"]} - > - <Icon name="clock" /> - {neverExpires || !expirationDate - ? "Never" - : expirationDate?.toLocaleDateString("sv-SE", { - year: "numeric", - month: "2-digit", - day: "2-digit", - })} - </Button> - } - /> - </div> + </Form> - {usersSharedWith.length > 0 && ( + {lastSharingAction.usersSharedWith?.length > 0 && ( <Message success> <Message.Header> {name} #{run} is now shared with </Message.Header> <Message.List> - {usersSharedWith.map((user_email, index) => ( - <Message.Item key={index}>{user_email}</Message.Item> + {lastSharingAction.usersSharedWith.map((userEmail, index) => ( + <Message.Item key={index}>{userEmail}</Message.Item> ))} </Message.List> </Message> )} - {usersNotSharedWith.length > 0 && ( + {lastSharingAction.usersNotSharedWith?.length > 0 && ( <Message error> <Message.Header> {name} #{run} could not be shared with </Message.Header> <Message.List> - {usersNotSharedWith.map((failure, index) => ( + {lastSharingAction.usersNotSharedWith.map((failure, index) => ( <Message.Item key={index}> - {failure.user_email_to_share_with}: {failure.error_message} + {failure.userEmailToShareWith}: {failure.errorMessage} </Message.Item> ))} </Message.List> </Message> )} - {userUnsharedWith && ( + {lastSharingAction?.userUnsharedWith && ( <Message success> - {name} #{run} is no longer shared with {userUnsharedWith} + {name} #{run} is no longer shared with{" "} + {lastSharingAction.userUnsharedWith} </Message> )} - - {unshareError && ( + {lastSharingAction?.unshareError && ( <Message error> - An error occurred while unsharing {name} #{run}: {unshareError} + An error occurred while unsharing {name} #{run}:{" "} + {lastSharingAction.unshareError} </Message> )} @@ -345,7 +421,6 @@ export default function WorkflowShareModal() { onClose={closeExpirationModal} closeIcon size="mini" - id={styles["expiration-date-modal"]} > <Modal.Header>Set expiration date</Modal.Header> <Modal.Content> @@ -368,102 +443,28 @@ export default function WorkflowShareModal() { minDate={new Date()} /> </Modal.Content> - <Modal.Actions id={styles["expiration-modal-actions"]}> + <Modal.Actions className={styles["expiration-modal-actions"]}> <Button primary onClick={handleSetExpirationDate} - className="aligned left" - id={styles["expiration-modal-set-button"]} + className={styles["expiration-modal-set-button"]} > Set </Button> </Modal.Actions> </Modal> - {loadingWorkflowUnshare && ( - <Loader inline id={styles["loader-unshare-workflow"]} inverted /> - )} - - <h3 id={styles["read-only-header"]}>Read-only access</h3> - - {workflowShareStatusLoading ? ( - <Loader inline id={styles["loader-share-status"]} inverted /> - ) : ( - <> - {workflowShares.length > 0 ? ( - <div> - {workflowShares.map((share, index) => ( - <div key={index} id={styles["share-item"]}> - <div> - <Icon - name="user" - size="big" - id={styles["share-user-icon"]} - /> - {share.user_email} - </div> - <div> - <Popup - content={"Expires"} - position="top center" - disabled={false} - trigger={ - <span> - <Button - size="tiny" - icon - labelPosition="left" - disabled - > - <Icon name="clock" /> - {share.valid_until - ? share.valid_until.substring(0, 10) - : "Never"} - </Button> - </span> - } - /> - - <Popup - content={"Unshare"} - position="top center" - trigger={ - <Button - size="tiny" - icon - negative - onClick={() => { - setConfirmUnshareOpen(true); - setUserToUnshareWith(share.user_email); - }} - > - <Icon name="trash" /> - </Button> - } - /> - <Confirm - size="mini" - open={confirmUnshareOpen} - header="Unshare workflow" - content={`Are you sure you want to unshare ${name} #${run} with ${userToUnshareWith}?`} - onCancel={() => setConfirmUnshareOpen(false)} - onConfirm={() => - handleUnshareWorkflow(userToUnshareWith) - } - /> - </div> - </div> - ))} - </div> - ) : ( - <Message info id={styles["no-share-message"]}> - This workflow has not been shared with anyone yet. - </Message> - )} - </> - )} + <WorkflowShareStatus + workflow={workflow} + handleUnshareWorkflowError={(err) => { + setLastSharingAction({ unshareError: err }); + }} + handleUnshareWorkflowSuccess={(user) => { + setLastSharingAction({ userUnsharedWith: user }); + }} + /> </Modal.Content> - <Modal.Actions id={styles["sharing-modal-actions"]}> + <Modal.Actions className={styles["sharing-modal-actions"]}> <Popup content={"Link copied!"} open={linkCopied} @@ -473,15 +474,14 @@ export default function WorkflowShareModal() { icon labelPosition="left" onClick={copyCurrentUrl} - className="left floated" - id={styles["copy-link-button"]} + className={`left floated ${styles["copy-link-button"]}`} > <Icon name="linkify" /> Copy link </Button> } /> - <Button onClick={onCloseModal} id={styles["done-button"]}> + <Button onClick={onCloseModal} className={styles["done-button"]}> Done </Button> </Modal.Actions> diff --git a/reana-ui/src/components/WorkflowShareModal.module.scss b/reana-ui/src/components/WorkflowShareModal.module.scss index 5259bad8..9da7fd83 100644 --- a/reana-ui/src/components/WorkflowShareModal.module.scss +++ b/reana-ui/src/components/WorkflowShareModal.module.scss @@ -8,93 +8,54 @@ under the terms of the MIT License; see LICENSE file for more details. */ -#workflow-share-modal > i { - top: 1.0535rem; - right: 1rem; - color: rgba(0, 0, 0, 0.87); -} - -#expiration-date-modal > i { - top: 1.0535rem; - right: 1rem; - color: rgba(0, 0, 0, 0.87); -} - -#loader-share-workflow:before, -#loader-share-status:before, -#loader-unshare-workflow:before { - border-color: rgba(0, 0, 0, 0.1); -} - -#loader-share-workflow:after, -#loader-share-status:after, -#loader-unshare-workflow:after { +:global(.ui.dimmer) :global(.ui.loader).loader-share-status:after { border-color: #767676 transparent transparent; } -#loader-share-workflow { - margin-left: 5px; -} - -#loader-unshare-workflow { - margin-top: 1.5em; -} - -#message { - margin: 1em 0; -} - -#share-buttons { +.share-buttons { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5em; } -#share-button { - display: flex; - align-items: center; -} - -#expiration-date-button { +:global(.ui.button).expiration-date-button { margin-right: 0; } -#expiration-modal-actions { +:global(.ui.modal) > :global(.actions).expiration-modal-actions { text-align: left; + padding: 1rem 1.5rem; } -#expiration-modal-set-button { - margin-left: 0.5em; +:global(.ui.modal) + :global(.actions) + > :global(.button).expiration-modal-set-button { + margin-left: 0; } -#read-only-header { - margin-top: 0.75em; -} - -#share-item { +.share-item { display: flex; justify-content: space-between; align-items: center; margin-top: 0.5em; } -#share-user-icon { +:global(i.icon).share-user-icon { margin-right: 0.25em; } -#no-share-message { - margin-top: -0.25em; +:global(.ui.modal) > :global(.actions).sharing-modal-actions { + padding-left: 1.5em; + padding-right: 1.5em; } -#sharing-modal-actions { - padding: 1.5em; -} - -#copy-link-button { - margin-left: 0px; -} +:global(.ui.modal) :global(.actions) > :global(.button) { + &.copy-link-button { + margin-left: 0px; + } -#done-button { - margin-right: 0px; + &.done-button { + margin-right: 0px; + } } diff --git a/reana-ui/src/pages/workflowList/WorkflowList.js b/reana-ui/src/pages/workflowList/WorkflowList.js index ab053841..e5e6ca22 100644 --- a/reana-ui/src/pages/workflowList/WorkflowList.js +++ b/reana-ui/src/pages/workflowList/WorkflowList.js @@ -73,12 +73,22 @@ function Workflows() { useEffect(() => cleanPolling(), [workflowRefresh]); useEffect(() => { + let shared = false; + let sharedBy = null; + if (ownedByFilter === "anybody") { + shared = true; + sharedBy = null; + } else if (ownedByFilter !== "you") { + sharedBy = ownedByFilter; + } + dispatch( fetchWorkflows({ pagination: { ...pagination }, search: searchFilter, status: statusFilter, - ownedBy: ownedByFilter, + shared, + sharedBy, sharedWith: sharedWithFilter, sort: sortDir, }), @@ -87,12 +97,14 @@ function Workflows() { if (!interval.current && reanaToken && pollingSecs) { interval.current = setInterval(() => { const showLoader = false; + dispatch( fetchWorkflows({ pagination: { ...pagination }, search: searchFilter, status: statusFilter, - ownedBy: ownedByFilter, + shared, + sharedBy, sharedWith: sharedWithFilter, sort: sortDir, showLoader, @@ -141,7 +153,7 @@ function Workflows() { return ( <div className={styles.container}> - <Container id={styles["workflow-list-container"]}> + <Container text className={styles["workflow-list-container"]}> <Title className={styles.title}> <span>Your workflows</span> <span className={styles.refresh}> diff --git a/reana-ui/src/pages/workflowList/WorkflowList.module.scss b/reana-ui/src/pages/workflowList/WorkflowList.module.scss index 44699bd7..0b89d426 100644 --- a/reana-ui/src/pages/workflowList/WorkflowList.module.scss +++ b/reana-ui/src/pages/workflowList/WorkflowList.module.scss @@ -42,37 +42,6 @@ margin: 2em; } -#workflow-list-container { - font-size: 1.14285714rem; - line-height: 1.5; - margin-left: 0; - margin-right: 0; -} - -/* Mobile */ -@media only screen and (max-width: 767px) { - #workflow-list-container { - width: 500px; - } -} - -/* Tablet */ -@media only screen and (min-width: 768px) and (max-width: 991px) { - #workflow-list-container { - width: 700px; - } -} - -/* Small Monitor */ -@media only screen and (min-width: 992px) and (max-width: 1199px) { - #workflow-list-container { - width: 800px; - } -} - -/* Large Monitor */ -@media only screen and (min-width: 1200px) { - #workflow-list-container { - width: 825px; - } +:global(.ui.text.container).workflow-list-container { + max-width: 825px !important; } diff --git a/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.js b/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.js index d8632d82..c43e497d 100644 --- a/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.js +++ b/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.js @@ -17,9 +17,12 @@ import { fetchUsersSharedWithYou, fetchUsersYouSharedWith } from "~/actions"; import { getUsersSharedWithYou, getUsersYouSharedWith } from "~/selectors"; import styles from "./WorkflowSharingFilter.module.scss"; +const OWNED_BY = "owned_by"; +const SHARED_WITH = "shared_with"; + const sharingFilterOptions = [ - { key: 0, text: "Owned by", value: "owned_by" }, - { key: 1, text: "Shared with", value: "shared_with" }, + { key: 0, text: "Owned by", value: OWNED_BY }, + { key: 1, text: "Shared with", value: SHARED_WITH }, ]; export default function WorkflowSharingFilters({ @@ -29,10 +32,7 @@ export default function WorkflowSharingFilters({ setSharedWithFilter, }) { const dispatch = useDispatch(); - const [selectedFilterOption, setSelectedFilterOption] = useState( - sharingFilterOptions[0], - ); - const [dynamicOptions, setDynamicOptions] = useState([]); + const [selectedFilterOption, setSelectedFilterOption] = useState(OWNED_BY); const usersYouSharedWith = useSelector(getUsersYouSharedWith, _.isEqual); const usersSharedWithYou = useSelector(getUsersSharedWithYou, _.isEqual); @@ -62,7 +62,9 @@ export default function WorkflowSharingFilters({ [usersYouSharedWith], ); - const [selectedUser, setSelectedUser] = useState(); + const [selectedUser, setSelectedUser] = useState( + usersSharedWithYouOptions[0].value, + ); useEffect(() => { dispatch(fetchUsersYouSharedWith()); @@ -70,19 +72,16 @@ export default function WorkflowSharingFilters({ }, [dispatch]); useEffect(() => { - setDynamicOptions(usersSharedWithYouOptions); - }, [usersSharedWithYou, usersSharedWithYouOptions]); - - useEffect(() => { - if (selectedFilterOption.value === sharingFilterOptions[0].value) { - if (!isEqual(ownedByFilter, selectedUser?.value)) { + // synchronise local state with parent state + if (selectedFilterOption === OWNED_BY) { + if (!isEqual(ownedByFilter, selectedUser)) { setSharedWithFilter(undefined); - setOwnedByFilter(selectedUser?.value); + setOwnedByFilter(selectedUser); } } else { - if (!isEqual(sharedWithFilter, selectedUser?.value)) { + if (!isEqual(sharedWithFilter, selectedUser)) { + setSharedWithFilter(selectedUser); setOwnedByFilter(undefined); - setSharedWithFilter(selectedUser?.value); } } }, [ @@ -91,54 +90,47 @@ export default function WorkflowSharingFilters({ setOwnedByFilter, setSharedWithFilter, selectedUser, - selectedFilterOption.value, + selectedFilterOption, ]); const handleSelectedFilterOptionChange = (_, { value }) => { - const selectedOption = sharingFilterOptions.find( - (option) => option.value === value, - ); - setSelectedFilterOption(selectedOption); - - if (value === sharingFilterOptions[0].value) { - setDynamicOptions(usersSharedWithYouOptions); - setSelectedUser(usersSharedWithYouOptions[0]); + setSelectedFilterOption(value); + + if (value === OWNED_BY) { + setSelectedUser(usersSharedWithYouOptions[0].value); } else { - setDynamicOptions(usersYouSharedWithOptions); - setSelectedUser(usersYouSharedWithOptions[0]); + setSelectedUser(usersYouSharedWithOptions[0].value); } }; const handleSelectedUserChange = (_, { value }) => { - const selectedUser = dynamicOptions.find( - (option) => option.value === value, - ); - setSelectedUser(selectedUser); + setSelectedUser(value); }; return ( <Grid.Column mobile={16} tablet={7} computer={6}> <div style={{ display: "flex" }}> <Dropdown - text={selectedFilterOption.text} fluid - closeOnChange selection options={sharingFilterOptions} onChange={handleSelectedFilterOptionChange} - defaultValue={selectedFilterOption.value} - id={styles["sharing-filter-dropdown"]} + value={selectedFilterOption} + className={styles["sharing-filter-dropdown"]} /> <Dropdown - text={selectedUser?.text || "you"} fluid selection search scrolling - options={dynamicOptions} + options={ + selectedFilterOption === OWNED_BY + ? usersSharedWithYouOptions + : usersYouSharedWithOptions + } onChange={handleSelectedUserChange} - value={selectedUser?.value || "you"} - id={styles["selected-user-dropdown"]} + value={selectedUser} + className={styles["selected-user-dropdown"]} /> </div> </Grid.Column> diff --git a/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.module.scss b/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.module.scss index 979d8bed..ccc6068e 100644 --- a/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.module.scss +++ b/reana-ui/src/pages/workflowList/components/WorkflowSharingFilter.module.scss @@ -8,39 +8,20 @@ under the terms of the MIT License; see LICENSE file for more details. */ -#sharing-filter-dropdown { - border-top-right-radius: 0%; - border-bottom-right-radius: 0%; +:global(.ui.selection.dropdown).sharing-filter-dropdown { + border-top-right-radius: 0; + border-bottom-right-radius: 0; } -#sharing-filter-dropdown:hover, -#sharing-filter-dropdown:focus, -#sharing-filter-dropdown:active { - z-index: 2; -} - -#selected-user-dropdown { - border-top-left-radius: 0%; - border-bottom-left-radius: 0%; - margin-left: -1px; - overflow: hidden; -} - -#selected-user-dropdown.input { +:global(.ui.selection.dropdown).selected-user-dropdown { border-top-left-radius: 0%; border-bottom-left-radius: 0%; - margin-left: -1px; -} - -#selected-user-dropdown > div[aria-atomic="true"] { - max-width: 105px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - height: 14+2px; - margin-bottom: -2px; -} - -#selected-user-dropdown[aria-expanded="true"] { - overflow: visible; + border-left: 0; + + > div[aria-atomic="true"] { + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } } diff --git a/reana-ui/src/reducers.js b/reana-ui/src/reducers.js index 49d1b6a3..45328679 100644 --- a/reana-ui/src/reducers.js +++ b/reana-ui/src/reducers.js @@ -45,24 +45,13 @@ import { OPEN_STOP_WORKFLOW_MODAL, CLOSE_STOP_WORKFLOW_MODAL, WARNING, - USERS_SHARED_WITH_YOU_FETCH, USERS_SHARED_WITH_YOU_RECEIVED, - USERS_SHARED_WITH_YOU_FETCH_ERROR, - USERS_YOU_SHARED_WITH_FETCH, USERS_YOU_SHARED_WITH_RECEIVED, - USERS_YOU_SHARED_WITH_FETCH_ERROR, OPEN_SHARE_WORKFLOW_MODAL, CLOSE_SHARE_WORKFLOW_MODAL, WORKFLOW_SHARE_STATUS_FETCH, WORKFLOW_SHARE_STATUS_RECEIVED, - WORKFLOW_SHARE_INIT, - WORKFLOW_SHARED_SUCCESSFULLY, - WORKFLOW_SHARED_ERROR, - WORKFLOW_SHARE_FINISH, WORKFLOW_SHARE_STATUS_FETCH_ERROR, - WORKFLOW_UNSHARE_INIT, - WORKFLOW_UNSHARED, - WORKFLOW_UNSHARE_ERROR, } from "~/actions"; import { USER_ERROR } from "./errors"; @@ -135,6 +124,7 @@ const sharingInitialState = { usersWorkflowWasSharedWith: [], usersWorkflowWasNotSharedWith: [], userWorkflowWasUnsharedWith: null, + sharedWith: {}, }; const notification = (state = notificationInitialState, action) => { @@ -319,22 +309,6 @@ const workflows = (state = workflowsInitialState, action) => { return { ...state, workflowShareModal: { open: false, workflow: null } }; case WORKFLOW_LIST_REFRESH: return { ...state, workflowRefresh: Math.random() }; - case WORKFLOW_SHARE_STATUS_FETCH: - return { ...state, loadingWorkflowShareStatus: true }; - case WORKFLOW_SHARE_STATUS_RECEIVED: - return { - ...state, - workflows: { - ...state.workflows, - [action.id]: { - ...state.workflows[action.id], - sharedWith: action.workflow_share_status, - }, - }, - loadingWorkflowShareStatus: false, - }; - case WORKFLOW_SHARE_STATUS_FETCH_ERROR: - return { ...state, loadingWorkflowShareStatus: false }; default: return state; @@ -414,66 +388,32 @@ const quota = (state = quotaInitialState, action) => { const sharing = (state = sharingInitialState, action) => { switch (action.type) { - case USERS_SHARED_WITH_YOU_FETCH: - return { ...state }; case USERS_SHARED_WITH_YOU_RECEIVED: return { ...state, - usersSharedWithYou: action.users_shared_with_you, - }; - case USERS_SHARED_WITH_YOU_FETCH_ERROR: - return { ...state }; - case USERS_YOU_SHARED_WITH_FETCH: - return { - ...state, + usersSharedWithYou: action.usersSharedYouWith, }; case USERS_YOU_SHARED_WITH_RECEIVED: return { ...state, - usersYouSharedWith: action.users_you_shared_with, - }; - case USERS_YOU_SHARED_WITH_FETCH_ERROR: - return { ...state }; - case WORKFLOW_SHARE_INIT: - return { - ...state, - loadingWorkflowShare: true, - usersWorkflowWasSharedWith: [], - usersWorkflowWasNotSharedWith: [], - }; - case WORKFLOW_SHARED_SUCCESSFULLY: - return { - ...state, - usersWorkflowWasSharedWith: action.users_shared_with, + usersYouSharedWith: action.usersYouSharedWith, }; - case WORKFLOW_SHARED_ERROR: - return { - ...state, - usersWorkflowWasNotSharedWith: action.users_not_shared_with, - }; - case WORKFLOW_SHARE_FINISH: - return { - ...state, - loadingWorkflowShare: false, - }; - case WORKFLOW_UNSHARE_INIT: - return { - ...state, - unshareError: null, - loadingWorkflowUnshare: true, - }; - case WORKFLOW_UNSHARED: - return { - ...state, - loadingWorkflowUnshare: false, - userWorkflowWasUnsharedWith: action.user_email_to_unshare_with, - }; - case WORKFLOW_UNSHARE_ERROR: + case WORKFLOW_SHARE_STATUS_FETCH: + return { ...state, loadingWorkflowShareStatus: true }; + case WORKFLOW_SHARE_STATUS_RECEIVED: return { ...state, - loadingWorkflowUnshare: false, - unshareError: action.error_message, + sharedWith: { + ...state.sharedWith, + [action.id]: { + ...state.sharedWith[action.id], + sharedWith: action.sharedWith, + }, + }, + loadingWorkflowShareStatus: false, }; + case WORKFLOW_SHARE_STATUS_FETCH_ERROR: + return { ...state, loadingWorkflowShareStatus: false }; default: return state; diff --git a/reana-ui/src/selectors.js b/reana-ui/src/selectors.js index c4f26a0f..d6b42503 100644 --- a/reana-ui/src/selectors.js +++ b/reana-ui/src/selectors.js @@ -24,7 +24,6 @@ export const getUserQuota = (state) => state.quota; // Auth export const isSignedIn = (state) => !!state.auth.email; -export const getUserId = (state) => state.auth.id; export const getUserEmail = (state) => state.auth.email; export const getUserFullName = (state) => state.auth.fullName; export const getUserFetchError = (state) => state.auth.error[USER_ERROR.fetch]; @@ -60,12 +59,6 @@ export const getWorkflowShareModalOpen = (state) => state.workflows.workflowShareModal.open; export const getWorkflowShareModalItem = (state) => state.workflows.workflowShareModal.workflow; -export const getWorkflowShareStatus = (id) => (state) => - state.workflows.workflows && - state.workflows.workflows[id] && - state.workflows.workflows[id].sharedWith; -export const getLoadingWorkflowShareStatus = (state) => - state.workflows && state.workflows.loadingWorkflowShareStatus; export const getWorkflowRefresh = (state) => state.workflows.workflowRefresh; // Details @@ -100,3 +93,9 @@ export const getUnshareError = (state) => state.sharing && state.sharing.unshareError; export const getUserWorkflowWasUnsharedWith = (state) => state.sharing && state.sharing.userWorkflowWasUnsharedWith; +export const getWorkflowShareStatus = (id) => (state) => + state.sharing.sharedWith && + state.sharing.sharedWith[id] && + state.sharing.sharedWith[id].sharedWith; +export const getLoadingWorkflowShareStatus = (state) => + state.sharing && state.sharing.loadingWorkflowShareStatus; diff --git a/reana-ui/src/util.js b/reana-ui/src/util.js index 1416cfce..ecb5c1b7 100644 --- a/reana-ui/src/util.js +++ b/reana-ui/src/util.js @@ -60,6 +60,7 @@ export function parseWorkflows(workflows) { workflow.running = typeof running === "object" ? running.total : 0; workflow.failed = typeof failed === "object" ? failed.total : 0; workflow.launcherURL = workflow.launcher_url; + workflow.ownerEmail = workflow.owner_email; workflow = parseWorkflowDates(workflow); obj[workflow.id] = workflow;