From b02399b809ea4b310dc13328676abae7248fa303 Mon Sep 17 00:00:00 2001 From: lflangis Date: Mon, 8 Jan 2024 10:16:33 -0500 Subject: [PATCH] feat(cavatica): SKFP-895 move cavatica widgets to ferlab-ui --- package-lock.json | 27 +- package.json | 2 +- src/App.tsx | 7 +- src/common/fenceTypes.ts | 10 +- .../Cavatica/AnalyzeButton/index.tsx | 200 ++++++----- .../Cavatica/AnalyzeModal/index.module.scss | 45 --- .../Cavatica/AnalyzeModal/index.tsx | 211 ------------ .../CreateProjectModal/index.module.scss | 5 - .../Cavatica/CreateProjectModal/index.tsx | 123 ------- src/components/Icons/CavaticaIcon.tsx | 24 -- .../uiKit/Fences/Modal/index.module.scss | 10 - src/components/uiKit/Fences/Modal/index.tsx | 90 ----- src/helpers/fence.ts | 5 - src/locales/en.ts | 23 +- src/services/api/cavatica/index.ts | 4 +- src/services/api/fence/index.ts | 13 +- src/services/api/fence/models.ts | 2 +- src/store/fenceCavatica/index.ts | 22 -- src/store/fenceCavatica/selector.ts | 8 - src/store/fenceCavatica/slice.ts | 160 --------- src/store/fenceCavatica/thunks.tsx | 312 ------------------ src/store/fenceCavatica/types.ts | 40 --- src/store/fenceConnection/index.ts | 29 -- src/store/fenceConnection/selector.ts | 8 - src/store/fenceConnection/slice.ts | 119 ------- src/store/fenceConnection/thunks.ts | 122 ------- src/store/fenceConnection/types.ts | 35 -- src/store/fenceStudies/index.ts | 27 -- src/store/fenceStudies/selector.ts | 8 - src/store/fenceStudies/slice.ts | 48 --- src/store/fenceStudies/thunks.ts | 261 --------------- src/store/fenceStudies/types.ts | 31 -- src/store/fences/index.ts | 14 +- src/store/fences/selector.ts | 28 +- src/store/fences/slice.ts | 27 +- src/store/fences/thunks.ts | 38 ++- src/store/fences/types.ts | 6 +- src/store/index.tsx | 8 +- src/store/passport/index.ts | 31 ++ src/store/passport/selector.ts | 4 + src/store/passport/slice.ts | 147 +++++++++ src/store/passport/thunks.ts | 286 ++++++++++++++++ src/store/passport/type.ts | 33 ++ src/store/report/thunks.tsx | 2 +- src/store/types.ts | 12 +- src/tests/setupTests.tsx | 10 +- src/utils/routes.ts | 2 +- .../AuthorizedStudies/index.test.tsx | 1 - .../AuthorizedStudies/index.tsx | 7 +- .../Cavatica/ListItem/index.module.scss | 40 --- .../Cavatica/ListItem/index.tsx | 42 --- .../DashboardCards/Cavatica/index.module.scss | 39 --- .../DashboardCards/Cavatica/index.tsx | 219 ++++++------ .../DashboardCards/Notebook/index.tsx | 100 ++++-- src/views/Dashboard/index.tsx | 3 - .../PageContent/tabs/DataFiles/index.tsx | 19 +- src/views/FenceRedirect/index.tsx | 4 +- src/views/FileEntity/Title/index.tsx | 15 +- .../FileEntity/utils/getSummaryItems.tsx | 6 +- 59 files changed, 952 insertions(+), 2222 deletions(-) delete mode 100644 src/components/Cavatica/AnalyzeModal/index.module.scss delete mode 100644 src/components/Cavatica/AnalyzeModal/index.tsx delete mode 100644 src/components/Cavatica/CreateProjectModal/index.module.scss delete mode 100644 src/components/Cavatica/CreateProjectModal/index.tsx delete mode 100644 src/components/Icons/CavaticaIcon.tsx delete mode 100644 src/components/uiKit/Fences/Modal/index.module.scss delete mode 100644 src/components/uiKit/Fences/Modal/index.tsx delete mode 100644 src/helpers/fence.ts delete mode 100644 src/store/fenceCavatica/index.ts delete mode 100644 src/store/fenceCavatica/selector.ts delete mode 100644 src/store/fenceCavatica/slice.ts delete mode 100644 src/store/fenceCavatica/thunks.tsx delete mode 100644 src/store/fenceCavatica/types.ts delete mode 100644 src/store/fenceConnection/index.ts delete mode 100644 src/store/fenceConnection/selector.ts delete mode 100644 src/store/fenceConnection/slice.ts delete mode 100644 src/store/fenceConnection/thunks.ts delete mode 100644 src/store/fenceConnection/types.ts delete mode 100644 src/store/fenceStudies/index.ts delete mode 100644 src/store/fenceStudies/selector.ts delete mode 100644 src/store/fenceStudies/slice.ts delete mode 100644 src/store/fenceStudies/thunks.ts delete mode 100644 src/store/fenceStudies/types.ts create mode 100644 src/store/passport/index.ts create mode 100644 src/store/passport/selector.ts create mode 100644 src/store/passport/slice.ts create mode 100644 src/store/passport/thunks.ts create mode 100644 src/store/passport/type.ts delete mode 100644 src/views/Dashboard/components/DashboardCards/Cavatica/ListItem/index.module.scss delete mode 100644 src/views/Dashboard/components/DashboardCards/Cavatica/ListItem/index.tsx delete mode 100644 src/views/Dashboard/components/DashboardCards/Cavatica/index.module.scss diff --git a/package-lock.json b/package-lock.json index 5495941a8..ccfdd9131 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@babel/core": "^7.16.0", "@dnd-kit/core": "^4.0.3", "@dnd-kit/sortable": "^5.1.0", - "@ferlab/ui": "^7.19.3", + "@ferlab/ui": "^7.20.2", "@loadable/component": "^5.15.2", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", "@react-keycloak/core": "^3.2.0", @@ -2926,9 +2926,9 @@ } }, "node_modules/@ferlab/ui": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@ferlab/ui/-/ui-7.19.3.tgz", - "integrity": "sha512-fTj+SZVSeXuNLbQvsY+SfdP2yUi6As97kSMAw+UGwg6SV/LIGtk1TmB/qAO8jma19S8ry7uvXxufN92AMSaQ3Q==", + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@ferlab/ui/-/ui-7.20.2.tgz", + "integrity": "sha512-uo00foPivS1zKZJFoVnRUcdWGwnJG1O0mtNZfol2qoc6SEZodzAU4ersRzu8Zzq9Pf9/6yDpPct3zyk5WTALMQ==", "dependencies": { "@ant-design/icons": "^4.7.0", "@dnd-kit/core": "^4.0.3", @@ -2959,6 +2959,7 @@ "peerDependencies": { "antd": "^4.24.11", "date-fns": "^2.29.3", + "rc-tree-select": "^5.4.0", "react": "^17.0.2 || ^18.0.0", "react-dom": "^17.0.2 || ^18.0.0", "react-router-dom": "^5.2.0" @@ -28555,9 +28556,9 @@ } }, "node_modules/react-grid-layout/node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", "engines": { "node": ">=6" } @@ -36539,9 +36540,9 @@ "dev": true }, "@ferlab/ui": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@ferlab/ui/-/ui-7.19.3.tgz", - "integrity": "sha512-fTj+SZVSeXuNLbQvsY+SfdP2yUi6As97kSMAw+UGwg6SV/LIGtk1TmB/qAO8jma19S8ry7uvXxufN92AMSaQ3Q==", + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@ferlab/ui/-/ui-7.20.2.tgz", + "integrity": "sha512-uo00foPivS1zKZJFoVnRUcdWGwnJG1O0mtNZfol2qoc6SEZodzAU4ersRzu8Zzq9Pf9/6yDpPct3zyk5WTALMQ==", "requires": { "@ant-design/icons": "^4.7.0", "@dnd-kit/core": "^4.0.3", @@ -55978,9 +55979,9 @@ }, "dependencies": { "clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==" } } }, diff --git a/package.json b/package.json index 4fc1ae229..c78900ec8 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@babel/core": "^7.16.0", "@dnd-kit/core": "^4.0.3", "@dnd-kit/sortable": "^5.1.0", - "@ferlab/ui": "^7.19.3", + "@ferlab/ui": "^7.20.2", "@loadable/component": "^5.15.2", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.3", "@react-keycloak/core": "^3.2.0", diff --git a/src/App.tsx b/src/App.tsx index 41ccce4fe..b59079e94 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { Switch, } from 'react-router-dom'; import Empty from '@ferlab/ui/core/components/Empty'; +import { PASSPORT } from '@ferlab/ui/core/components/Widgets/Cavatica'; import MaintenancePage from '@ferlab/ui/core/pages/MaintenancePage'; import { setLocale } from '@ferlab/ui/core/utils/localeUtils'; import loadable from '@loadable/component'; @@ -39,9 +40,9 @@ import SideImageLayout from 'components/Layout/SideImage'; import GradientAccent from 'components/uiKit/GradientAccent'; import Spinner from 'components/uiKit/Spinner'; import NotificationContextHolder from 'components/utils/NotificationContextHolder'; +import { initGa } from 'services/analytics'; import { useLang } from 'store/global'; import { DYNAMIC_ROUTES, STATIC_ROUTES } from 'utils/routes'; -import { initGa } from 'services/analytics'; const loadableProps = { fallback: }; const Dashboard = loadable(() => import('views/Dashboard'), loadableProps); @@ -94,9 +95,9 @@ const App = () => { render={() => } /> } + render={() => } /> diff --git a/src/common/fenceTypes.ts b/src/common/fenceTypes.ts index 822252a95..2eb5c9e51 100644 --- a/src/common/fenceTypes.ts +++ b/src/common/fenceTypes.ts @@ -1,22 +1,14 @@ export enum FENCE_NAMES { dcf = 'dcf', gen3 = 'gen3', - cavatica = 'cavatica', } -export const ALL_FENCE_NAMES = [FENCE_NAMES.gen3, FENCE_NAMES.dcf, FENCE_NAMES.cavatica]; +export const ALL_FENCE_NAMES = [FENCE_NAMES.gen3, FENCE_NAMES.dcf]; export const ALL_STUDIES_FENCE_NAMES = [FENCE_NAMES.gen3, FENCE_NAMES.dcf]; -export enum FENCE_CONNECTION_STATUSES { - connected = 'connected', - disconnected = 'disconnected', - unknown = 'unknown', -} - export type TFenceConnections = { [FENCE_NAMES.dcf]?: TConnection; [FENCE_NAMES.gen3]?: TConnection; - [FENCE_NAMES.cavatica]?: TConnection; }; export type TProjects = { [index: string]: any }; diff --git a/src/components/Cavatica/AnalyzeButton/index.tsx b/src/components/Cavatica/AnalyzeButton/index.tsx index 39a935283..5e6345c3f 100644 --- a/src/components/Cavatica/AnalyzeButton/index.tsx +++ b/src/components/Cavatica/AnalyzeButton/index.tsx @@ -1,19 +1,25 @@ import { useCallback, useEffect } from 'react'; import intl from 'react-intl-universal'; import { useDispatch } from 'react-redux'; -import { CloudUploadOutlined } from '@ant-design/icons'; +import CavaticaAnalyse from '@ferlab/ui/core/components/Widgets/Cavatica/CavaticaAnalyse'; +import { + CAVATICA_ANALYSE_STATUS, + PASSPORT_AUTHENTIFICATION_STATUS, +} from '@ferlab/ui/core/components/Widgets/Cavatica/type'; import { BooleanOperators } from '@ferlab/ui/core/data/sqon/operators'; import { ISqonGroupFilter } from '@ferlab/ui/core/data/sqon/types'; -import { Button, Modal } from 'antd'; -import { CAVATICA_FILE_BATCH_SIZE } from 'views/Studies/utils/constant'; +import { CAVATICA_FILE_BATCH_SIZE } from 'views/DataExploration/utils/constant'; -import { FENCE_NAMES } from 'common/fenceTypes'; -import AnalyzeModal from 'components/Cavatica/AnalyzeModal'; -import CreateProjectModal from 'components/Cavatica/CreateProjectModal'; -import { useFenceCavatica } from 'store/fenceCavatica'; -import { fenceCavaticaActions } from 'store/fenceCavatica/slice'; -import { beginAnalyse } from 'store/fenceCavatica/thunks'; -import { connectToFence } from 'store/fenceConnection/thunks'; +import { CavaticaApi } from 'services/api/cavatica'; +import { ICavaticaCreateProjectBody } from 'services/api/cavatica/models'; +import { fetchAllFencesAuthentificationStatus } from 'store/fences/thunks'; +import { useCavaticaPassport } from 'store/passport'; +import { passportActions } from 'store/passport/slice'; +import { + beginCavaticaAnalyse, + connectCavaticaPassport, + createCavaticaProjet, +} from 'store/passport/thunks'; interface OwnProps { fileIds: string[]; @@ -28,14 +34,13 @@ const CavaticaAnalyzeButton: React.FC = ({ type = 'default', disabled = false, }) => { - const { isConnected, isInitializingAnalyse, beginAnalyseAfterConnection } = useFenceCavatica(); - const dispatch = useDispatch(); + const cavatica = useCavaticaPassport(); const onBeginAnalyse = useCallback( () => dispatch( - beginAnalyse({ + beginCavaticaAnalyse({ sqon: sqon || { op: BooleanOperators.and, content: [], @@ -47,72 +52,117 @@ const CavaticaAnalyzeButton: React.FC = ({ ); useEffect(() => { - if (isConnected && beginAnalyseAfterConnection) { + dispatch(fetchAllFencesAuthentificationStatus()); + }, []); + + // If the user is not connected to cavatica + useEffect(() => { + if ( + cavatica.authentification.status === PASSPORT_AUTHENTIFICATION_STATUS.connected && + cavatica.bulkImportData.status === CAVATICA_ANALYSE_STATUS.pending_analyse + ) { onBeginAnalyse(); } - }, [isConnected, beginAnalyseAfterConnection, onBeginAnalyse]); - - const onCavaticaUploadLimitReached = () => - Modal.error({ - title: intl.get('screen.dataExploration.tabs.datafiles.cavatica.bulkImportLimit.title'), - content: intl.getHTML( - 'screen.dataExploration.tabs.datafiles.cavatica.bulkImportLimit.description', - { - limit: CAVATICA_FILE_BATCH_SIZE, - }, - ), - okText: 'Ok', - cancelText: undefined, - }); - - const onCavaticaConnectionRequired = () => - Modal.confirm({ - type: 'warn', - title: intl.get('screen.dataExploration.tabs.datafiles.cavatica.authWarning.title'), - content: intl.get('screen.dataExploration.tabs.datafiles.cavatica.authWarning.description'), - okText: intl.get('screen.dataExploration.tabs.datafiles.cavatica.authWarning.connect'), - onOk: () => { - dispatch(fenceCavaticaActions.setBeginAnalyseConnectionFlag()); - dispatch(connectToFence(FENCE_NAMES.cavatica)); - }, - }); + }, [cavatica.authentification.status, cavatica.bulkImportData.status, onBeginAnalyse]); return ( - <> - - {isConnected && ( - <> - - - - )} - + { + dispatch(passportActions.setCavaticaBulkImportDataStatus(status)); + }} + handleConnection={() => { + dispatch(connectCavaticaPassport()); + }} + handleResetErrors={() => { + dispatch(passportActions.resetCavaticaBillingsGroupError()); + dispatch(passportActions.resetCavaticaProjectsError()); + dispatch(passportActions.setCavaticaBulkImportDataStatus(CAVATICA_ANALYSE_STATUS.unknow)); + }} + handleCreateProject={(values: ICavaticaCreateProjectBody) => { + dispatch( + createCavaticaProjet({ + body: values, + }), + ); + }} + dictionary={{ + analyseModal: { + copyFiles: intl.get( + 'screen.dataExploration.tabs.datafiles.cavatica.analyseModal.copyFiles', + ), + copyFilesTo: intl.get( + 'screen.dataExploration.tabs.datafiles.cavatica.analyseModal.copyFilesTo', + ), + createProjectToPushFileTo: intl.get( + 'screen.dataExploration.tabs.datafiles.cavatica.analyseModal.createProjectToPushFileTo', + ), + newProject: intl.get( + 'screen.dataExploration.tabs.datafiles.cavatica.analyseModal.newProject', + ), + title: intl.get('screen.dataExploration.tabs.datafiles.cavatica.analyseInCavatica'), + youAreAuthorizedToCopy: intl.get( + 'screen.dataExploration.tabs.datafiles.cavatica.analyseModal.youAreAuthorizedToCopy', + ), + files: intl.get('screen.dataExploration.tabs.datafiles.cavatica.analyseModal.files'), + ofFiles: intl.get('screen.dataExploration.tabs.datafiles.cavatica.analyseModal.ofFiles'), + }, + billingGroupsErrorModal: { + description: intl.get('api.cavatica.error.projects.fetch'), + title: intl.get('api.cavatica.error.title'), + }, + buttonText: intl.get('screen.dataExploration.tabs.datafiles.cavatica.analyseInCavatica'), + connectionRequiredModal: { + description: intl.get( + 'screen.dataExploration.tabs.datafiles.cavatica.authWarning.description', + ), + okText: intl.get('screen.dataExploration.tabs.datafiles.cavatica.authWarning.connect'), + title: intl.get('screen.dataExploration.tabs.datafiles.cavatica.authWarning.title'), + }, + fetchErrorModal: { + description: intl.get('api.cavatica.error.bulk.fetchfiles'), + title: intl.get('api.cavatica.error.title'), + }, + createProjectModal: { + title: intl.get('screen.dashboard.cards.cavatica.newProject'), + requiredField: intl.get('global.forms.errors.requiredField'), + projectName: { + label: 'Project name', + placeholder: 'e.g. KF-NBL Neuroblastoma Aligned Reads', + }, + billingGroup: { + label: intl.get('screen.dashboard.cards.cavatica.billingGroups.label'), + empty: intl.get('screen.dashboard.cards.cavatica.billingGroups.empty'), + }, + okText: intl.get('screen.dashboard.cards.cavatica.createProject'), + cancelText: intl.get('screen.dashboard.cards.cavatica.cancelText'), + }, + projectCreateErrorModal: { + description: intl.get('api.cavatica.error.projects.fetch'), + title: intl.get('api.cavatica.error.title'), + }, + projectFetchErrorModal: { + description: intl.get('api.cavatica.error.projects.fetch'), + title: intl.get('api.cavatica.error.title'), + }, + unauthorizedModal: { + description: intl.get('api.cavatica.error.fileAuth.description'), + title: intl.get('api.cavatica.error.fileAuth.title'), + }, + uploadLimitReachedModalError: { + description: intl.getHTML( + 'screen.dataExploration.tabs.datafiles.cavatica.bulkImportLimit.description', + { limit: CAVATICA_FILE_BATCH_SIZE }, + ), + okText: 'OK', + title: intl.get('screen.dataExploration.tabs.datafiles.cavatica.bulkImportLimit.title'), + }, + }} + /> ); }; diff --git a/src/components/Cavatica/AnalyzeModal/index.module.scss b/src/components/Cavatica/AnalyzeModal/index.module.scss deleted file mode 100644 index e552c96d2..000000000 --- a/src/components/Cavatica/AnalyzeModal/index.module.scss +++ /dev/null @@ -1,45 +0,0 @@ -.cavaticaAnalyseModal { - .authorizedFilesTag { - margin: 0; - } - - .studiesList { - div[class$='-meta-description'] { - margin-right: 12px; - } - - :global(.ant-list-item) { - &:hover { - background-color: transparent; - } - } - } -} - -.cavaticaModalWrapper { - .treeSelectorWrapper, - .treeSelector { - width: 100%; - } -} - -.cavaticaTreeDropdown { - padding: 0; - - .treeSelectEmpty { - width: 100%; - padding: 4px 8px; - } - - .cavaticaTreeDropdownMenu { - padding: 8px 4px; - } - - .cavaticaTreeDropdownFooter { - padding: 12px; - } - - .cavaticaTreeDropdownDivider { - margin: 0; - } -} diff --git a/src/components/Cavatica/AnalyzeModal/index.tsx b/src/components/Cavatica/AnalyzeModal/index.tsx deleted file mode 100644 index f777d416d..000000000 --- a/src/components/Cavatica/AnalyzeModal/index.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import { FileTextOutlined, PlusOutlined } from '@ant-design/icons'; -import { Button, Divider, List, Modal, Space, Tag, TreeSelect, Typography } from 'antd'; -import { IFileEntity } from 'graphql/files/models'; -import { groupBy } from 'lodash'; -import { LegacyDataNode } from 'rc-tree-select/lib/TreeSelect'; -import { useEffect, useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { CavaticaApi } from 'services/api/cavatica'; -import { CAVATICA_TYPE } from 'services/api/cavatica/models'; -import { useFenceCavatica } from 'store/fenceCavatica'; -import { fenceCavaticaActions } from 'store/fenceCavatica/slice'; -import { fetchAllProjects, startBulkImportJob } from 'store/fenceCavatica/thunks'; -import { ICavaticaTreeNode } from 'store/fenceCavatica/types'; -import intl from 'react-intl-universal'; - -import styles from './index.module.scss'; - -const { Text } = Typography; - -const AnalyseModal = () => { - const { - isAnalyseModalOpen, - projectsTree, - isLoading, - bulkImportData, - newlyCreatedProject, - isBulkImportLoading, - } = useFenceCavatica(); - const dispatch = useDispatch(); - const [selectedTreeNode, setSelectedTreeNode] = useState(); - const [localProjectTree, setLocalProjectTree] = useState([]); - const [dropdownOpen, setDropdownOpen] = useState(false); - - const handleOnCancel = () => { - setLocalProjectTree(projectsTree); - setSelectedTreeNode(undefined); - dispatch(fenceCavaticaActions.cancelAnalyse()); - }; - - const handleOnOk = () => dispatch(startBulkImportJob(selectedTreeNode!)); - - const handleOnClear = () => setSelectedTreeNode(undefined); - - const resetTreeAndSelection = () => { - setSelectedTreeNode(undefined); - setLocalProjectTree(projectsTree); - }; - - const handleCreateProject = () => { - setDropdownOpen(false); - // Need a timeout else the dropdown stay visible - // when the create project modal open - setTimeout(() => { - dispatch(fenceCavaticaActions.beginCreateProject()); - }, 100); - }; - - const NewProjectButton = () => ( - - ); - - useEffect(() => { - if (isAnalyseModalOpen) { - resetTreeAndSelection(); - dispatch(fetchAllProjects()); - } - // eslint-disable-next-line - }, [isAnalyseModalOpen]); - - useEffect(() => { - setLocalProjectTree(projectsTree); - }, [projectsTree]); - - useEffect(() => { - if (newlyCreatedProject) { - setSelectedTreeNode(newlyCreatedProject); - } - }, [newlyCreatedProject]); - - const onLoadData = async (node: LegacyDataNode) => { - const { data } = await CavaticaApi.listFilesAndFolders( - node.id, - node.type === CAVATICA_TYPE.PROJECT, - ); - - const childs = - data?.items - .filter(({ type }) => type !== CAVATICA_TYPE.FILE) - .map((child) => { - let enrichedChild: any = { - ...child, - value: child.id, - title: child.name, - pId: node.id, - }; - - return enrichedChild; - }) || []; - - setLocalProjectTree([...localProjectTree, ...childs]); - }; - - return ( - - - - - {intl.get('screen.dataExploration.tabs.datafiles.cavatica.analyseModal.copyFilesTo')} - - - - {intl.get( - 'screen.dataExploration.tabs.datafiles.cavatica.analyseModal.createProjectToPushFileTo', - )} - - - - } - onDropdownVisibleChange={setDropdownOpen} - showSearch - value={selectedTreeNode?.value} - dropdownClassName={styles.cavaticaTreeDropdown} - dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} - placeholder="Select a project" - allowClear - onClear={handleOnClear} - onSelect={(_: any, node: any /* ICavaticaTreeNode */) => { - setSelectedTreeNode(node); - }} - loadData={onLoadData} - treeData={localProjectTree} - dropdownRender={(menu) => ( - <> -
{menu}
- {localProjectTree.length > 0 && ( - <> - -
- -
- - )} - - )} - /> -
- - - {intl.get( - 'screen.dataExploration.tabs.datafiles.cavatica.analyseModal.youAreAuthorizedToCopy', - )} - {' '} - }> - {bulkImportData.authorizedFiles.length} files - {' '} - (out of {bulkImportData.files.length} selected) to your Cavatica workspace. - - { - return ( - - {item.title}} /> - - {item.nbFiles} - - - ); - }} - /> - -
- ); -}; - -const aggregateFilesToStudy = (filesToCopy: IFileEntity[]) => - Object.entries(groupBy(filesToCopy, 'study.study_id')).map((study) => ({ - title: study[1][0].study.study_name, - nbFiles: study[1].length, - })); - -export default AnalyseModal; diff --git a/src/components/Cavatica/CreateProjectModal/index.module.scss b/src/components/Cavatica/CreateProjectModal/index.module.scss deleted file mode 100644 index 3fd6363c4..000000000 --- a/src/components/Cavatica/CreateProjectModal/index.module.scss +++ /dev/null @@ -1,5 +0,0 @@ -.cavaticaCreateProjectModal { - .billingGroupItem { - margin-bottom: 0; - } -} diff --git a/src/components/Cavatica/CreateProjectModal/index.tsx b/src/components/Cavatica/CreateProjectModal/index.tsx deleted file mode 100644 index d46676168..000000000 --- a/src/components/Cavatica/CreateProjectModal/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useEffect, useState } from 'react'; -import intl from 'react-intl-universal'; -import { useDispatch } from 'react-redux'; -import Empty from '@ferlab/ui/core/components/Empty'; -import { Form, Input, Modal, Select } from 'antd'; - -import { useFenceCavatica } from 'store/fenceCavatica'; -import { fenceCavaticaActions } from 'store/fenceCavatica/slice'; -import { createProjet, fetchAllBillingGroups } from 'store/fenceCavatica/thunks'; - -import styles from './index.module.scss'; - -interface OwnProps { - openAnalyseModalOnClose?: boolean; - showSuccessNotification?: boolean; - showErrorNotification?: boolean; -} - -enum FORM_FIELDS { - PROJECT_NAME = 'project_name', - PROJECT_BILLING_GROUP = 'project_billing_group', -} - -const CreateProjectModal = ({ - openAnalyseModalOnClose = true, - showSuccessNotification = false, - showErrorNotification = true, -}: OwnProps) => { - const [form] = Form.useForm(); - const dispatch = useDispatch(); - const [isFormValid, setFormValid] = useState(false); - const { isCreateProjectModalOpen, isFetchingBillingGroup, billingGroups, isCreatingProject } = - useFenceCavatica(); - const handleOnCancel = () => { - form.resetFields(); - setFormValid(false); - dispatch(fenceCavaticaActions.cancelCreateProject(openAnalyseModalOnClose)); - }; - - const handleOnOk = () => { - form.submit(); - }; - - useEffect(() => { - if (isCreateProjectModalOpen) { - dispatch(fetchAllBillingGroups()); - } - // eslint-disable-next-line - }, [isCreateProjectModalOpen]); - - return ( - -
{ - dispatch( - createProjet({ - showErrorNotification, - showSuccessNotification, - openAnalyseModalOnDone: openAnalyseModalOnClose, - body: { - billing_group: values[FORM_FIELDS.PROJECT_BILLING_GROUP], - name: values[FORM_FIELDS.PROJECT_NAME], - }, - }), - ); - }} - onFieldsChange={(f) => setFormValid(!f.some((item) => item.errors!.length > 0))} - validateMessages={{ - required: intl.get('global.forms.errors.requiredField'), - }} - fields={[ - { - name: [FORM_FIELDS.PROJECT_BILLING_GROUP], - value: billingGroups.length === 1 ? billingGroups[0].id : undefined, - }, - ]} - > - - - - - - -
-
- ); -}; - -export default CreateProjectModal; diff --git a/src/components/Icons/CavaticaIcon.tsx b/src/components/Icons/CavaticaIcon.tsx deleted file mode 100644 index a801eb464..000000000 --- a/src/components/Icons/CavaticaIcon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import cx from "classnames"; -import { IconProps } from "components/Icons"; - -const CavaticaIcon = ({ - className = "", - width = 40, - height = 40, -}: IconProps) => ( - - - -); -export default CavaticaIcon; diff --git a/src/components/uiKit/Fences/Modal/index.module.scss b/src/components/uiKit/Fences/Modal/index.module.scss deleted file mode 100644 index 14f3a978c..000000000 --- a/src/components/uiKit/Fences/Modal/index.module.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import 'src/style/themes/kids-first/colors'; - -.item { - padding: 16px; - display: flex; - align-items: center; - background-color: $gray-2; - justify-content: space-between; - margin-top: 24px; -} diff --git a/src/components/uiKit/Fences/Modal/index.tsx b/src/components/uiKit/Fences/Modal/index.tsx deleted file mode 100644 index e8c3289f1..000000000 --- a/src/components/uiKit/Fences/Modal/index.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useDispatch } from 'react-redux'; -import { Modal, Button, Switch } from 'antd'; -import { useFenceConnection } from 'store/fenceConnection'; -import { fenceConnectionActions } from 'store/fenceConnection/slice'; -import { connectToFence, disconnectFromFence } from 'store/fenceConnection/thunks'; -import intl from 'react-intl-universal'; -import KidsFirstLoginIcon from 'components/Icons/KidsFirstLoginIcon'; -import NciIcon from 'components/Icons/NciIcon'; - -import style from './index.module.scss'; -import { FENCE_CONNECTION_STATUSES, FENCE_NAMES } from 'common/fenceTypes'; -import { useEffect } from 'react'; -import { globalActions } from 'store/global'; - -const iconSize = { - width: 45, - height: 45, -}; - -const FencesConnectionModal = () => { - const dispatch = useDispatch(); - const { modalConnectionParams, connectionStatus, fencesConnectError, loadingFences } = - useFenceConnection(); - - const isDcfConnected = connectionStatus[FENCE_NAMES.dcf] === FENCE_CONNECTION_STATUSES.connected; - const isGen3Connected = - connectionStatus[FENCE_NAMES.gen3] === FENCE_CONNECTION_STATUSES.connected; - - const onClose = () => { - if (modalConnectionParams.onClose) { - modalConnectionParams.onClose(); - } - dispatch(fenceConnectionActions.setConnectionModalParams({ open: false, onClose: undefined })); - }; - const onSwitchClick = (fenceNames: FENCE_NAMES, isConnected: boolean) => () => { - dispatch(isConnected ? disconnectFromFence(fenceNames) : connectToFence(fenceNames)); - }; - - useEffect(() => { - if (fencesConnectError.length > 0) { - dispatch( - globalActions.displayNotification({ - type: 'error', - message: intl.get('screen.dashboard.cards.authorizedStudies.notification.message'), - description: intl.get( - 'screen.dashboard.cards.authorizedStudies.notification.description', - ), - onClose: () => dispatch(fenceConnectionActions.resetFenceConnectErrors()), - }), - ); - } - }); - - return ( - - {intl.get('global.close')} - , - ]} - > - <> -

{intl.get('screen.dashboard.cards.authorizedStudies.modal.description')}

-
- - Kids First Framework Services - -
-
- - NCI CRDC Framework Services - -
- -
- ); -}; - -export default FencesConnectionModal; diff --git a/src/helpers/fence.ts b/src/helpers/fence.ts deleted file mode 100644 index 684172c1c..000000000 --- a/src/helpers/fence.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { FENCE_CONNECTION_STATUSES } from 'common/fenceTypes'; -import { TFencesConnectionStatus } from 'store/fenceConnection/types'; - -export const hasOneFenceConnected = (fences: TFencesConnectionStatus) => - Object.values(fences).some((fence) => fence === FENCE_CONNECTION_STATUSES.connected); diff --git a/src/locales/en.ts b/src/locales/en.ts index 93d4785e4..d9fd03c59 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -552,7 +552,7 @@ const en = { error: { title: 'Connection error', subtitle: - 'We are currently unable to connect to this service. Please refresh the page and try again. If the problem persists, please ', + 'We are currently unable to connect to this service. Please refresh the page and try again. If the problem persists, please', contactSupport: 'contact support', }, datarelease: { @@ -595,7 +595,7 @@ const en = { disconnect: 'Disconnect', noProjects: 'You do not have any Cavatica projects.', createNewProject: 'Create your first project', - membersCount: '{count, plural, =0 {member} =1 {# member} other {# members}}', + membersCount: '{count, plural, =0 {# member} =1 {# member} other {# members}}', infoPopover: { title: 'CAVATICA Compute Cloud Platform', content: @@ -603,6 +603,19 @@ const en = { readMore: 'Read more', }, newProject: 'New project', + billingGroups: { + label: 'Project billing group', + empty: 'You have no project billing group', + }, + createProject: 'Create project', + cancelText: 'Cancel', + error: { + create: { + title: 'Unable to create project', + subtitle: + 'An error has occurred and we were unable to create your project. Please try again or', + }, + }, }, savedFilters: { title: 'Saved Filters', @@ -1069,6 +1082,8 @@ const en = { connect: 'Connect', }, analyseModal: { + files: '{files} files', + ofFiles: '(out of {files} selected) to your Cavatica workspace.', newProject: 'New project', copyFiles: 'Copy files', copyFilesTo: 'Copy files to...', @@ -1413,7 +1428,7 @@ const en = { fileAuthorization: 'File Authorization', fileReadMore: 'applying for data access.', locked: - 'You are unauthorized to access this file. Users requesting access to controlled data require an eRA Commons account and permissions from an associated Data Access Committee. Read more on ', + 'You are unauthorized to access this file. Users requesting access to controlled data require an eRA Commons account and permissions from an associated Data Access Committee. Read more on', unlocked: 'You are authorized to access and copy this file to your Cavatica workspace.', data_access: { title: 'Data Access', @@ -1437,7 +1452,7 @@ const en = { format: 'Format', hash: 'Hash', locked: - 'You are unauthorized to access this file. Users requesting access to controlled data require an eRA Commons account and permissions from an associated Data Access Committee. Read more on ', + 'You are unauthorized to access this file. Users requesting access to controlled data require an eRA Commons account and permissions from an associated Data Access Committee. Read more on', manifest: 'Manifest', participants: 'Participants', participant_sample: 'Participant / Sample', diff --git a/src/services/api/cavatica/index.ts b/src/services/api/cavatica/index.ts index a262eda57..9d606b974 100644 --- a/src/services/api/cavatica/index.ts +++ b/src/services/api/cavatica/index.ts @@ -1,5 +1,7 @@ import EnvironmentVariables from 'helpers/EnvVariables'; + import { sendRequest } from 'services/api'; + import { ICavaticaBillingGroup, ICavaticaCreateProjectBody, @@ -7,8 +9,8 @@ import { ICavaticaDRSImportJobPayload, ICavaticaListPayload, ICavaticaProject, - ICavaticaProjectNode, ICavaticaProjectMember, + ICavaticaProjectNode, } from './models'; const KEY_MANAGER_API_URL = EnvironmentVariables.configFor('KEY_MANAGER_API_URL'); diff --git a/src/services/api/fence/index.ts b/src/services/api/fence/index.ts index e04358694..475935600 100644 --- a/src/services/api/fence/index.ts +++ b/src/services/api/fence/index.ts @@ -1,3 +1,4 @@ +import { PASSPORT } from '@ferlab/ui/core/components/Widgets/Cavatica'; import EnvironmentVariables from 'helpers/EnvVariables'; import { ARRANGER_API } from 'provider/ApolloProvider'; @@ -15,6 +16,8 @@ import { export const FENCE_API_URL = EnvironmentVariables.configFor('FENCE_API_URL'); +type TApiFence = FENCE_NAMES | PASSPORT; + const fetchAuthorizedStudies = (params: IAuthorizedStudiesFetchParams) => sendRequest({ method: 'POST', @@ -22,25 +25,25 @@ const fetchAuthorizedStudies = (params: IAuthorizedStudiesFetchParams) => data: params, }); -const isAuthenticated = (fence: FENCE_NAMES) => +const isAuthenticated = (fence: TApiFence) => sendRequest({ url: `${FENCE_API_URL}/${fence}/authenticated`, method: 'GET', }); -const fetchAcls = (fence: FENCE_NAMES) => +const fetchAcls = (fence: TApiFence) => sendRequest({ url: `${FENCE_API_URL}/${fence}/acl`, method: 'GET', }); -const fetchInfo = (fence: FENCE_NAMES) => +const fetchInfo = (fence: TApiFence) => sendRequest({ url: `${FENCE_API_URL}/${fence}/info`, method: 'GET', }); -const exchangeCode = (fence: FENCE_NAMES, code: string) => +const exchangeCode = (fence: TApiFence, code: string) => sendRequest({ url: `${FENCE_API_URL}/${fence}/exchange`, method: 'GET', @@ -49,7 +52,7 @@ const exchangeCode = (fence: FENCE_NAMES, code: string) => }, }); -const disconnect = (fence: FENCE_NAMES) => +const disconnect = (fence: TApiFence) => sendRequest({ url: `${FENCE_API_URL}/${fence}/token`, method: 'DELETE', diff --git a/src/services/api/fence/models.ts b/src/services/api/fence/models.ts index 10ef0dfc8..8afc1c2cb 100644 --- a/src/services/api/fence/models.ts +++ b/src/services/api/fence/models.ts @@ -1,4 +1,4 @@ -import { IAuthorizedStudy } from '@ferlab/ui/core/components/AuthorizedStudies'; +import { IAuthorizedStudy } from '@ferlab/ui/core/components/Widgets/AuthorizedStudies'; export interface IFenceAuthPayload { authenticated: boolean; diff --git a/src/store/fenceCavatica/index.ts b/src/store/fenceCavatica/index.ts deleted file mode 100644 index e16eeddcd..000000000 --- a/src/store/fenceCavatica/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { FENCE_CONNECTION_STATUSES, FENCE_NAMES } from 'common/fenceTypes'; -import { useSelector } from 'react-redux'; -import { useFenceConnection } from 'store/fenceConnection'; -import { fenceCavaticaSelector } from './selector'; - -export type { initialState as FenceCavaticaInitialState } from './types'; -export { default, FenceCavaticaState } from './slice'; -export const useFenceCavatica = () => { - const state = useSelector(fenceCavaticaSelector); - const { connectionStatus, loadingFences, fencesConnectError } = useFenceConnection( - FENCE_NAMES.cavatica, - ); - const isConnected = (status: FENCE_CONNECTION_STATUSES) => - status === FENCE_CONNECTION_STATUSES.connected; - - return { - ...state, - isConnected: isConnected(connectionStatus.cavatica), - connectionLoading: loadingFences.includes(FENCE_NAMES.cavatica), - hasErrors: fencesConnectError.includes(FENCE_NAMES.cavatica), - }; -}; diff --git a/src/store/fenceCavatica/selector.ts b/src/store/fenceCavatica/selector.ts deleted file mode 100644 index cd7290aea..000000000 --- a/src/store/fenceCavatica/selector.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RootState } from 'store/types'; -import { initialState } from 'store/fenceCavatica/types'; - -export type FenceCavaticaProps = initialState; - -export const fenceCavaticaSelector = (state: RootState) => { - return state.fenceCavatica; -}; diff --git a/src/store/fenceCavatica/slice.ts b/src/store/fenceCavatica/slice.ts deleted file mode 100644 index b57ad4ecc..000000000 --- a/src/store/fenceCavatica/slice.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { CAVATICA_TYPE, ICavaticaProject } from 'services/api/cavatica/models'; -import { initialState, TCavaticaProjectWithMembers } from 'store/fenceCavatica/types'; -import { - beginAnalyse, - createProjet, - fetchAllBillingGroups, - fetchAllProjects, - startBulkImportJob, -} from './thunks'; - -export const FenceCavaticaState: initialState = { - isAnalyseModalOpen: false, - isCreateProjectModalOpen: false, - isCreatingProject: false, - isFetchingBillingGroup: false, - isInitializingAnalyse: false, - isBulkImportLoading: false, - isLoading: false, - beginAnalyseAfterConnection: false, - bulkImportData: { - files: [], - authorizedFiles: [], - }, - projects: [], - projectsTree: [], - billingGroups: [], - newlyCreatedProject: undefined, -}; - -const convertProjectToTreeNode = (project: ICavaticaProject) => ({ - ...project, - pId: 0, - title: project.name, - value: project.id, - type: CAVATICA_TYPE.PROJECT, -}); - -const sortProjects = (projects: TCavaticaProjectWithMembers[]) => - projects.sort((p1, p2) => (new Date(p1.modified_on) < new Date(p2.modified_on) ? 1 : -1)); - -const fenceCavaticaSlice = createSlice({ - name: 'fenceCavatica', - initialState: FenceCavaticaState, - reducers: { - toggleAnalyseModal: (state, action: PayloadAction) => ({ - ...state, - isAnalyseModalOpen: action.payload, - }), - toggleCreateProjectModal: (state, action: PayloadAction) => ({ - ...state, - isCreateModalOpen: action.payload, - }), - setBeginAnalyseConnectionFlag: (state) => ({ - ...state, - beginAnalyseAfterConnection: true, - }), - beginCreateProject: (state) => ({ - ...state, - isAnalyseModalOpen: false, - isCreateProjectModalOpen: true, - }), - cancelCreateProject: (state, action: PayloadAction) => ({ - ...state, - isCreateProjectModalOpen: false, - isAnalyseModalOpen: action.payload, - }), - cancelAnalyse: (state) => ({ - ...state, - bulkImportData: { - files: [], - authorizedFiles: [], - }, - isAnalyseModalOpen: false, - }), - }, - extraReducers: (builder) => { - // FETCH PROJECTS - builder.addCase(fetchAllProjects.pending, (state, action) => { - state.isLoading = true; - }); - builder.addCase(fetchAllProjects.fulfilled, (state, action) => { - const sortedProjectList = sortProjects(action.payload); - state.isLoading = false; - state.projects = sortedProjectList; - state.projectsTree = sortedProjectList.map((project) => convertProjectToTreeNode(project)); - }); - builder.addCase(fetchAllProjects.rejected, (state, action) => { - state.isLoading = false; - state.error = action.payload; - }); - // FETCH BILLING GROUPS - builder.addCase(fetchAllBillingGroups.pending, (state, action) => { - state.isFetchingBillingGroup = true; - }); - builder.addCase(fetchAllBillingGroups.fulfilled, (state, action) => { - state.isFetchingBillingGroup = false; - state.billingGroups = action.payload; - }); - builder.addCase(fetchAllBillingGroups.rejected, (state, action) => { - state.isFetchingBillingGroup = false; - state.error = action.payload; - }); - // BEGIN ANALYSE - builder.addCase(beginAnalyse.pending, (state, action) => { - state.isInitializingAnalyse = true; - state.beginAnalyseAfterConnection = false; - state.newlyCreatedProject = undefined; - }); - builder.addCase(beginAnalyse.fulfilled, (state, action) => { - state.isInitializingAnalyse = false; - state.isAnalyseModalOpen = true; - state.bulkImportData = action.payload; - }); - builder.addCase(beginAnalyse.rejected, (state, action) => { - state.isInitializingAnalyse = false; - state.error = action.payload; - }); - // BULK IMPORT - builder.addCase(startBulkImportJob.pending, (state, action) => { - state.isBulkImportLoading = true; - }); - builder.addCase(startBulkImportJob.fulfilled, (state, action) => { - state.isBulkImportLoading = false; - state.isAnalyseModalOpen = false; - }); - builder.addCase(startBulkImportJob.rejected, (state, action) => { - state.isBulkImportLoading = false; - state.error = action.payload; - }); - // CREATE PROJECT - builder.addCase(createProjet.pending, (state, action) => { - state.isCreatingProject = true; - }); - builder.addCase(createProjet.fulfilled, (state, action) => { - const newProjectList = [ - ...state.projects, - { - ...action.payload.newProject, - memberCount: 1, - }, - ]; - const sortedProjectList = sortProjects(newProjectList); - - state.isCreatingProject = false; - state.isCreateProjectModalOpen = false; - state.isAnalyseModalOpen = action.payload.isAnalyseModalVisible; - state.newlyCreatedProject = convertProjectToTreeNode(action.payload.newProject); - state.projects = sortedProjectList; - state.projectsTree = sortedProjectList.map((project) => convertProjectToTreeNode(project)); - }); - builder.addCase(createProjet.rejected, (state, action) => { - state.isCreatingProject = false; - state.error = action.payload; - }); - }, -}); - -export const fenceCavaticaActions = fenceCavaticaSlice.actions; -export default fenceCavaticaSlice.reducer; diff --git a/src/store/fenceCavatica/thunks.tsx b/src/store/fenceCavatica/thunks.tsx deleted file mode 100644 index c8f142155..000000000 --- a/src/store/fenceCavatica/thunks.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; -import { Modal } from 'antd'; -import { chunk, isEmpty } from 'lodash'; -import { CavaticaApi } from 'services/api/cavatica'; -import { - CAVATICA_TYPE, - ICavaticaBillingGroup, - ICavaticaCreateProjectBody, - ICavaticaDRSImportItem, - ICavaticaProject, -} from 'services/api/cavatica/models'; -import { RootState } from 'store/types'; -import { IBulkImportData, ICavaticaTreeNode, TCavaticaProjectWithMembers } from './types'; -import intl from 'react-intl-universal'; -import { IFileEntity, IFileResultTree } from 'graphql/files/models'; -import { ISqonGroupFilter } from '@ferlab/ui/core/data/sqon/types'; -import { SEARCH_FILES_QUERY } from 'graphql/files/queries'; -import { termToSqon } from '@ferlab/ui/core/data/sqon/utils'; -import { BooleanOperators } from '@ferlab/ui/core/data/sqon/operators'; -import { CAVATICA_FILE_BATCH_SIZE } from 'views/DataExploration/utils/constant'; -import { handleThunkApiReponse } from 'store/utils'; -import EnvironmentVariables from 'helpers/EnvVariables'; -import { globalActions } from 'store/global'; -import { ArrangerApi } from 'services/api/arranger'; -import { userHasAccessToFile } from 'utils/dataFiles'; -import { FENCE_CONNECTION_STATUSES } from 'common/fenceTypes'; -import { hydrateResults } from '@ferlab/ui/core/graphql/utils'; - -const BATCH_SIZE = 100; -const USER_BASE_URL = EnvironmentVariables.configFor('CAVATICA_USER_BASE_URL'); - -const fetchAllProjects = createAsyncThunk< - TCavaticaProjectWithMembers[], - void, - { rejectValue: string; state: RootState } ->( - 'cavatica/fetch/projects', - async (_, thunkAPI) => { - const { data, error } = await CavaticaApi.fetchProjects(); - const projects = data?.items || []; - - if (error) { - thunkAPI.dispatch( - globalActions.displayNotification({ - type: 'error', - message: intl.get('api.cavatica.error.title'), - description: intl.get('api.cavatica.error.projects.fetch'), - }), - ); - return thunkAPI.rejectWithValue(error.message); - } - - const projectList = await Promise.all( - (projects || []).map(async (project) => { - const [memberResponse] = await Promise.all([CavaticaApi.fetchProjetMembers(project.id)]); - - return { ...project, memberCount: memberResponse.data?.items.length || 0 }; - }), - ); - - return projectList; - }, - { - condition: (_, { getState }) => { - const { fenceCavatica } = getState(); - - return isEmpty(fenceCavatica.projects); - }, - }, -); - -const beginAnalyse = createAsyncThunk< - IBulkImportData, - { - sqon: ISqonGroupFilter; - fileIds: string[]; - }, - { rejectValue: string; state: RootState } ->('cavatica/begin/analyse', async (args, thunkAPI) => { - const { fenceConnection } = thunkAPI.getState(); - const allFencesAcls = Object.values(fenceConnection.fencesAcls).flat(); - - const sqon: ISqonGroupFilter = { - op: BooleanOperators.and, - content: [args.sqon], - }; - - if (args.fileIds.length > 0) { - sqon.content = [ - ...sqon.content, - termToSqon({ - field: 'file_id', - value: args.fileIds, - }), - ]; - } - - const { data, error } = await ArrangerApi.graphqlRequest<{ data: IFileResultTree }>({ - query: SEARCH_FILES_QUERY.loc?.source.body, - variables: { - sqon, - first: CAVATICA_FILE_BATCH_SIZE, - }, - }); - - if (error) { - thunkAPI.dispatch( - globalActions.displayNotification({ - type: 'error', - message: intl.get('api.cavatica.error.title'), - description: intl.get('api.cavatica.error.bulk.fetchFiles'), - }), - ); - return thunkAPI.rejectWithValue(error.message); - } - - const files = hydrateResults(data?.data?.file?.hits?.edges || []); - - const authorizedFiles = getAuthorizedFiles( - allFencesAcls, - files, - fenceConnection.connectionStatus.cavatica === FENCE_CONNECTION_STATUSES.connected, - fenceConnection.connectionStatus.gen3 === FENCE_CONNECTION_STATUSES.connected, - ); - - if (!authorizedFiles.length) { - Modal.error({ - type: 'error', - title: intl.get('api.cavatica.error.fileAuth.title'), - content: intl.get('api.cavatica.error.fileAuth.description'), - }); - return thunkAPI.rejectWithValue('0 authorized files'); - } - - return { - files, - authorizedFiles, - }; -}); - -const fetchAllBillingGroups = createAsyncThunk< - ICavaticaBillingGroup[], - void, - { rejectValue: string; state: RootState } ->( - 'cavatica/fetch/billingGroups', - async (_, thunkAPI) => { - const { data, error } = await CavaticaApi.fetchBillingGroups(); - - return handleThunkApiReponse({ - error, - data: data?.items || [], - reject: thunkAPI.rejectWithValue, - onError: () => - thunkAPI.dispatch( - globalActions.displayNotification({ - type: 'error', - message: intl.get('api.cavatica.error.title'), - description: intl.get('api.cavatica.error.billingGroups.fetch'), - }), - ), - }); - }, - { - condition: (_, { getState }) => { - const { fenceCavatica } = getState(); - - return isEmpty(fenceCavatica.billingGroups); - }, - }, -); - -const createProjet = createAsyncThunk< - { - isAnalyseModalVisible: boolean; - newProject: ICavaticaProject; - }, - { - openAnalyseModalOnDone?: boolean; - showSuccessNotification?: boolean; - showErrorNotification?: boolean; - body: ICavaticaCreateProjectBody; - }, - { rejectValue: string; state: RootState } ->('cavatica/create/project', async (args, thunkAPI) => { - const { data, error } = await CavaticaApi.createProject(args.body); - - return handleThunkApiReponse({ - error, - reject: thunkAPI.rejectWithValue, - onError: () => { - if (args.showErrorNotification) { - thunkAPI.dispatch( - globalActions.displayNotification({ - type: 'error', - message: intl.get('api.cavatica.error.title'), - description: intl.get('api.cavatica.error.projects.create'), - }), - ); - } - }, - onSuccess: () => { - if (args.showSuccessNotification) { - thunkAPI.dispatch( - globalActions.displayNotification({ - type: 'success', - message: intl.get('api.cavatica.success.title'), - description: intl.get('api.cavatica.success.projects.create'), - }), - ); - } - }, - data: { - isAnalyseModalVisible: args.openAnalyseModalOnDone || false, - newProject: data!, - }, - }); -}); - -const startBulkImportJob = createAsyncThunk< - any, - ICavaticaTreeNode, - { rejectValue: string; state: RootState } ->('cavatica/bulk/import', async (node, thunkAPI) => { - const { fenceCavatica } = thunkAPI.getState(); - const selectedFiles = fenceCavatica.bulkImportData.authorizedFiles; - const isProject = node.type === CAVATICA_TYPE.PROJECT; - - const indexFileDrsItems: ICavaticaDRSImportItem[] = []; - const cavaticaDrsItems: ICavaticaDRSImportItem[] = selectedFiles.map((file) => { - const destKey = isProject ? 'project' : 'parent'; - - if (file.index) { - indexFileDrsItems.push({ - drs_uri: file.index.urls, - name: file.index.file_name, - [destKey]: node.id, - }); - } - - return { - drs_uri: file.access_urls, - name: file.file_name, - [destKey]: node.id, - metadata: { - fhir_document_reference: file.fhir_document_reference, - }, - }; - }); - - const drsItems = cavaticaDrsItems.concat(indexFileDrsItems); - const itemsChunks = chunk(drsItems, BATCH_SIZE); - - const responses = await Promise.all( - itemsChunks.map((itemsChunk) => CavaticaApi.startBulkDrsImportJob({ items: itemsChunk })), - ); - const responseWithError = responses.find((resp) => !!resp.error); - - return handleThunkApiReponse({ - error: responseWithError?.error, - data: true, - reject: thunkAPI.rejectWithValue, - onError: () => - thunkAPI.dispatch( - globalActions.displayNotification({ - type: 'error', - message: intl.get('api.cavatica.error.title'), - description: intl.get('api.cavatica.error.bulk.import'), - }), - ), - onSuccess: () => - thunkAPI.dispatch( - globalActions.displayNotification({ - type: 'success', - message: intl.get('api.cavatica.success.title'), - description: ( -
- {intl.getHTML('api.cavatica.success.bulk.import.copySuccess', { - destination: node.title, - })} -
-
- {intl.get('api.cavatica.success.bulk.import.possibleDelays')} -
-
- - {intl.get('api.cavatica.success.bulk.import.openProject')} - -
- ), - duration: 5, - }), - ), - }); -}); - -const getAuthorizedFiles = ( - userAcls: string[], - files: IFileEntity[], - isConnectedToCavatica: boolean, - isConnectedToGen3: boolean, -) => - files.filter((file) => - userHasAccessToFile(file, userAcls, isConnectedToCavatica, isConnectedToGen3), - ); - -export { fetchAllProjects, fetchAllBillingGroups, createProjet, beginAnalyse, startBulkImportJob }; diff --git a/src/store/fenceCavatica/types.ts b/src/store/fenceCavatica/types.ts deleted file mode 100644 index 79043f475..000000000 --- a/src/store/fenceCavatica/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { IFileEntity, ITableFileEntity } from 'graphql/files/models'; -import { ICavaticaBillingGroup, ICavaticaProject } from 'services/api/cavatica/models'; - -export type initialState = { - isAnalyseModalOpen: boolean; - isCreateProjectModalOpen: boolean; - isCreatingProject: boolean; - isFetchingBillingGroup: boolean; - isInitializingAnalyse: boolean; - isBulkImportLoading: boolean; - isLoading: boolean; - beginAnalyseAfterConnection: boolean; - bulkImportData: IBulkImportData; - projects: TCavaticaProjectWithMembers[]; - projectsTree: ICavaticaTreeNode[]; - billingGroups: ICavaticaBillingGroup[]; - error?: any; - newlyCreatedProject?: ICavaticaTreeNode; -}; - -export interface IBulkImportData { - files: IFileEntity[]; - authorizedFiles: IFileEntity[]; -} - -export type TCavaticaProjectWithMembers = ICavaticaProject & { - memberCount: number; -}; - -export interface ICavaticaTreeNode { - href: string; - name: string; - id: string; - pId: any; - value: string; - title: string; - type: string; - project?: string; - isLeaf?: boolean; -} diff --git a/src/store/fenceConnection/index.ts b/src/store/fenceConnection/index.ts deleted file mode 100644 index 95caf7b49..000000000 --- a/src/store/fenceConnection/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; - -import { FENCE_NAMES } from 'common/fenceTypes'; - -import { fenceConnectionSelector } from './selector'; -import { checkFenceAuthStatus, checkFencesAuthStatus } from './thunks'; - -export type { initialState as FenceConnectionInitialState } from './types'; -export { default, FenceConnectionState } from './slice'; - -export const useFenceConnection = (fence?: FENCE_NAMES) => { - const dispatch = useDispatch(); - const state = useSelector(fenceConnectionSelector); - - useEffect(() => { - if (fence) { - dispatch(checkFenceAuthStatus(fence)); - } else { - dispatch(checkFencesAuthStatus()); - } - // eslint-disable-next-line - }, []); - - return { - ...state, - fencesAllAcls: Object.values(state.fencesAcls).flat(), - }; -}; diff --git a/src/store/fenceConnection/selector.ts b/src/store/fenceConnection/selector.ts deleted file mode 100644 index 941fcb686..000000000 --- a/src/store/fenceConnection/selector.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RootState } from 'store/types'; -import { initialState } from 'store/fenceConnection/types'; - -export type FenceConnectionProps = initialState; - -export const fenceConnectionSelector = (state: RootState) => { - return state.fenceConnection; -}; diff --git a/src/store/fenceConnection/slice.ts b/src/store/fenceConnection/slice.ts deleted file mode 100644 index 39deda8d1..000000000 --- a/src/store/fenceConnection/slice.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; - -import { FENCE_CONNECTION_STATUSES, FENCE_NAMES } from 'common/fenceTypes'; -import { initialState, TModalConnectionParams } from 'store/fenceConnection/types'; - -import { checkFenceAuthStatus, connectToFence, disconnectFromFence } from './thunks'; - -export const FenceConnectionState: initialState = { - connectionStatus: { - [FENCE_NAMES.dcf]: FENCE_CONNECTION_STATUSES.unknown, - [FENCE_NAMES.gen3]: FENCE_CONNECTION_STATUSES.unknown, - [FENCE_NAMES.cavatica]: FENCE_CONNECTION_STATUSES.unknown, - }, - fencesInfo: { - [FENCE_NAMES.dcf]: undefined, - [FENCE_NAMES.gen3]: undefined, - [FENCE_NAMES.cavatica]: undefined, - }, - fencesAcls: { - [FENCE_NAMES.dcf]: [], - [FENCE_NAMES.gen3]: [], - [FENCE_NAMES.cavatica]: [], - }, - loadingFences: [], - fencesConnectError: [], - fencesDisconnectError: [], - modalConnectionParams: { - open: false, - onClose: undefined, - }, -}; - -const removeFenceFromList = (state: FENCE_NAMES[], fenceName: FENCE_NAMES) => - state.filter((name) => name !== fenceName); - -const removeLoadingFences = (state: initialState, fenceName: FENCE_NAMES) => - state.loadingFences.filter((name) => name !== fenceName); - -const addLoadingFences = (state: initialState, fenceName: FENCE_NAMES) => - state.loadingFences.includes(fenceName) - ? state.loadingFences - : [...state.loadingFences, fenceName]; - -const fenceConnectionSlice = createSlice({ - name: 'fence', - initialState: FenceConnectionState, - reducers: { - setConnectionModalParams: (state, action: PayloadAction) => ({ - ...state, - modalConnectionParams: action.payload, - }), - resetFenceConnectErrors: (state) => ({ - ...state, - fencesConnectError: [], - }), - }, - extraReducers: (builder) => { - /** NEW */ - // CONNECT FENCE - builder.addCase(connectToFence.pending, (state, action) => { - state.loadingFences = addLoadingFences(state, action.meta.arg); - state.fencesConnectError = removeFenceFromList(state.fencesConnectError, action.meta.arg); - }); - builder.addCase(connectToFence.fulfilled, (state, action) => { - state.loadingFences = removeLoadingFences(state, action.meta.arg); - state.connectionStatus[action.meta.arg] = FENCE_CONNECTION_STATUSES.connected; - state.fencesInfo[action.meta.arg] = action.payload.info; - state.fencesAcls[action.meta.arg] = action.payload.acls; - }); - builder.addCase(connectToFence.rejected, (state, action) => { - state.loadingFences = removeLoadingFences(state, action.meta.arg); - state.fencesConnectError = [...state.fencesConnectError, action.meta.arg]; - state.connectionStatus[action.meta.arg] = FENCE_CONNECTION_STATUSES.disconnected; - // TODO add connections - }); - - // DISCONNECT FENCE - builder.addCase(disconnectFromFence.pending, (state, action) => { - state.loadingFences = addLoadingFences(state, action.meta.arg); - state.fencesDisconnectError = removeFenceFromList( - state.fencesDisconnectError, - action.meta.arg, - ); - }); - builder.addCase(disconnectFromFence.fulfilled, (state, action) => { - state.loadingFences = removeLoadingFences(state, action.meta.arg); - state.connectionStatus[action.meta.arg] = FENCE_CONNECTION_STATUSES.disconnected; - }); - builder.addCase(disconnectFromFence.rejected, (state, action) => { - state.loadingFences = removeLoadingFences(state, action.meta.arg); - state.fencesDisconnectError = [...state.fencesDisconnectError, action.meta.arg]; - state.connectionStatus[action.meta.arg] = FENCE_CONNECTION_STATUSES.disconnected; - }); - - // CHECK AUTH STATUS - builder.addCase(checkFenceAuthStatus.pending, (state, action) => { - state.loadingFences = addLoadingFences(state, action.meta.arg); - }); - builder.addCase(checkFenceAuthStatus.fulfilled, (state, action) => { - const isAuthenticated = action.payload && action.payload.auth?.authenticated; - state.loadingFences = removeLoadingFences(state, action.meta.arg); - - if (isAuthenticated) { - state.fencesAcls[action.meta.arg] = action.payload.acls; - } - - state.connectionStatus[action.meta.arg] = isAuthenticated - ? FENCE_CONNECTION_STATUSES.connected - : FENCE_CONNECTION_STATUSES.disconnected; - }); - builder.addCase(checkFenceAuthStatus.rejected, (state, action) => { - state.loadingFences = removeLoadingFences(state, action.meta.arg); - state.connectionStatus[action.meta.arg] = FENCE_CONNECTION_STATUSES.disconnected; - }); - }, -}); - -export const fenceConnectionActions = fenceConnectionSlice.actions; -export default fenceConnectionSlice.reducer; diff --git a/src/store/fenceConnection/thunks.ts b/src/store/fenceConnection/thunks.ts deleted file mode 100644 index 086abccde..000000000 --- a/src/store/fenceConnection/thunks.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; -import { RootState } from 'store/types'; -import { ALL_FENCE_NAMES, FENCE_CONNECTION_STATUSES, FENCE_NAMES } from 'common/fenceTypes'; -import { handleThunkApiReponse } from 'store/utils'; -import { FenceApi } from 'services/api/fence'; -import { IFenceAuthPayload, IFenceInfo } from 'services/api/fence/models'; - -const TEN_MINUTES_IN_MS = 1000 * 60 * 10; - -/* NEW NEW NEW */ - -const checkFencesAuthStatus = createAsyncThunk( - 'fence/checkAll/auth/status', - async (_, thunkAPI) => { - ALL_FENCE_NAMES.forEach(async (fence) => await thunkAPI.dispatch(checkFenceAuthStatus(fence))); - }, -); - -const checkFenceAuthStatus = createAsyncThunk< - { - auth: IFenceAuthPayload; - acls: string[]; - }, - FENCE_NAMES, - { state: RootState } ->( - 'fence/check/auth/status', - async (fence, thunkAPI) => { - let fenceAcls: string[] = []; - const { data, error } = await FenceApi.isAuthenticated(fence); - - if (data?.authenticated) { - const { data: aclData } = await FenceApi.fetchAcls(fence); - fenceAcls = aclData?.acl || []; - } - - return handleThunkApiReponse({ - error, - data: { - auth: data!, - acls: fenceAcls, - }, - reject: thunkAPI.rejectWithValue, - }); - }, - { - condition: (fence, { getState }) => { - const { fenceConnection } = getState(); - - return ( - !fenceConnection.loadingFences.includes(fence) && - fenceConnection.connectionStatus[fence] === FENCE_CONNECTION_STATUSES.unknown - ); - }, - }, -); - -const connectToFence = createAsyncThunk< - { - info: IFenceInfo; - acls: string[]; - }, - FENCE_NAMES, - { - state: RootState; - } ->('fence/connection', async (fence, thunkAPI) => { - const { fenceConnection } = thunkAPI.getState(); - let fenceInfo = fenceConnection.fencesInfo[fence]; - - if (!fenceInfo) { - const { data } = await FenceApi.fetchInfo(fence); - fenceInfo = data; - } - - const authWindow = window.open(fenceInfo?.authorize_uri)!; - - return new Promise((resolve, reject) => { - const interval = setInterval(async () => { - if (authWindow.closed) { - let fenceAcls: string[] = []; - const { data } = await FenceApi.isAuthenticated(fence); - - if (data?.authenticated) { - clearInterval(interval); - - if (data?.authenticated) { - const { data: aclData } = await FenceApi.fetchAcls(fence); - fenceAcls = aclData?.acl || []; - } - - resolve({ - info: fenceInfo!, - acls: fenceAcls, - }); - } else { - clearInterval(interval); - reject('failed authenticating'); - } - } - }, 1000); - setTimeout(() => { - clearInterval(interval); - reject('nothing'); - }, TEN_MINUTES_IN_MS); - }); -}); - -const disconnectFromFence = createAsyncThunk( - 'fence/disconnection', - async (fence, thunkAPI) => { - const { data, error } = await FenceApi.disconnect(fence); - - return handleThunkApiReponse({ - error, - data, - reject: thunkAPI.rejectWithValue, - }); - }, -); - -export { connectToFence, disconnectFromFence, checkFenceAuthStatus, checkFencesAuthStatus }; diff --git a/src/store/fenceConnection/types.ts b/src/store/fenceConnection/types.ts deleted file mode 100644 index a1cdd53ce..000000000 --- a/src/store/fenceConnection/types.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { FENCE_CONNECTION_STATUSES, FENCE_NAMES } from 'common/fenceTypes'; -import { IFenceInfo } from 'services/api/fence/models'; - -export type TFencesConnectionStatus = { - [FENCE_NAMES.dcf]: FENCE_CONNECTION_STATUSES; - [FENCE_NAMES.gen3]: FENCE_CONNECTION_STATUSES; - [FENCE_NAMES.cavatica]: FENCE_CONNECTION_STATUSES; -}; - -export type TModalConnectionParams = { - open: boolean; - onClose?: () => void; -}; - -export type initialState = { - loadingFences: FENCE_NAMES[]; - fencesConnectError: FENCE_NAMES[]; - fencesDisconnectError: FENCE_NAMES[]; - fencesInfo: { - [FENCE_NAMES.dcf]?: IFenceInfo; - [FENCE_NAMES.gen3]?: IFenceInfo; - [FENCE_NAMES.cavatica]?: IFenceInfo; - }; - fencesAcls: { - [FENCE_NAMES.dcf]: string[]; - [FENCE_NAMES.gen3]: string[]; - [FENCE_NAMES.cavatica]: string[]; - }; - connectionStatus: { - [FENCE_NAMES.dcf]: FENCE_CONNECTION_STATUSES; - [FENCE_NAMES.gen3]: FENCE_CONNECTION_STATUSES; - [FENCE_NAMES.cavatica]: FENCE_CONNECTION_STATUSES; - }; - modalConnectionParams: TModalConnectionParams; -}; diff --git a/src/store/fenceStudies/index.ts b/src/store/fenceStudies/index.ts deleted file mode 100644 index 0fbd1be96..000000000 --- a/src/store/fenceStudies/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useSelector } from 'react-redux'; -import isEmpty from 'lodash/isEmpty'; - -import { FENCE_CONNECTION_STATUSES, FENCE_NAMES } from 'common/fenceTypes'; -import { useFenceConnection } from 'store/fenceConnection'; - -import { fenceStudiesSelector } from './selector'; -import { computeAllFencesAuthStudies } from './thunks'; - -export type { initialState as fenceStudiesInitialState } from './types'; -export { default, FenceStudiesState } from './slice'; - -export const useFenceStudies = (fence: FENCE_NAMES) => { - const state = useSelector(fenceStudiesSelector); - - const { connectionStatus, fencesConnectError, loadingFences } = useFenceConnection(fence); - const isConnected = (status: FENCE_CONNECTION_STATUSES) => - status === FENCE_CONNECTION_STATUSES.connected; - - return { - ...state, - fenceStudiesAcls: computeAllFencesAuthStudies(state.studies), - isConnected: isConnected(connectionStatus[fence]), - connectionLoading: loadingFences.includes(fence), - hasErrors: fencesConnectError.includes(fence) || !isEmpty(state.fencesError), - }; -}; diff --git a/src/store/fenceStudies/selector.ts b/src/store/fenceStudies/selector.ts deleted file mode 100644 index a07d8c54a..000000000 --- a/src/store/fenceStudies/selector.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RootState } from "store/types"; -import { initialState } from "store/fenceStudies/types"; - -export type FenceStudiesProps = initialState; - -export const fenceStudiesSelector = (state: RootState) => { - return state.fenceStudies; -}; diff --git a/src/store/fenceStudies/slice.ts b/src/store/fenceStudies/slice.ts deleted file mode 100644 index b44bd4386..000000000 --- a/src/store/fenceStudies/slice.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit'; -import { FENCE_NAMES } from 'common/fenceTypes'; -import { initialState } from 'store/fenceStudies/types'; -import { fetchFenceStudies } from './thunks'; - -export const FenceStudiesState: initialState = { - studies: {}, - loadingStudiesForFences: [], - fencesError: [], -}; - -const removeFenceAuthError = (state: FENCE_NAMES[], fenceName: FENCE_NAMES) => - state.filter((name) => name !== fenceName); - -const removeLoadingFenceStudies = (state: initialState, fenceName: FENCE_NAMES) => - state.loadingStudiesForFences.filter((name) => name !== fenceName); - -const addLoadingFenceStudies = (state: initialState, fenceName: FENCE_NAMES) => - state.loadingStudiesForFences.includes(fenceName) - ? state.loadingStudiesForFences - : [...state.loadingStudiesForFences, fenceName]; - -const fenceStudiesSlice = createSlice({ - name: 'fenceStudies', - initialState: FenceStudiesState, - reducers: {}, - extraReducers: (builder) => { - // FETCH FENCE STUDIES - builder.addCase(fetchFenceStudies.pending, (state, action) => { - state.fencesError = removeFenceAuthError(state.fencesError, action.meta.arg.fenceName); - state.loadingStudiesForFences = addLoadingFenceStudies(state, action.meta.arg.fenceName); - }); - builder.addCase(fetchFenceStudies.fulfilled, (state, action) => { - state.loadingStudiesForFences = removeLoadingFenceStudies(state, action.meta.arg.fenceName); - state.studies = { - ...state.studies, - ...action.payload, - }; - }); - builder.addCase(fetchFenceStudies.rejected, (state, action) => { - state.loadingStudiesForFences = removeLoadingFenceStudies(state, action.meta.arg.fenceName); - state.fencesError = [...state.fencesError, action.meta.arg.fenceName]; - }); - }, -}); - -export const fenceStudiesActions = fenceStudiesSlice.actions; -export default fenceStudiesSlice.reducer; diff --git a/src/store/fenceStudies/thunks.ts b/src/store/fenceStudies/thunks.ts deleted file mode 100644 index f26eaa694..000000000 --- a/src/store/fenceStudies/thunks.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { BooleanOperators, TermOperators } from '@ferlab/ui/core/data/sqon/operators'; -import { createAsyncThunk } from '@reduxjs/toolkit'; -import { AxiosError } from 'axios'; -import { FileAccessType } from 'graphql/files/models'; -import { isEmpty } from 'lodash'; - -import { ALL_STUDIES_FENCE_NAMES, FENCE_CONNECTION_STATUSES, FENCE_NAMES } from 'common/fenceTypes'; -import { ArrangerApi } from 'services/api/arranger'; -import { RootState } from 'store/types'; -import { handleThunkApiReponse } from 'store/utils'; - -import { TFenceStudies, TFenceStudiesIdsAndCount, TFenceStudy } from './types'; - -const fetchAllFenceStudies = createAsyncThunk< - void, - void, - { rejectValue: string; state: RootState } ->('fenceStudies/fetch/all/studies', async (args, thunkAPI) => { - const { fenceConnection } = thunkAPI.getState(); - - ALL_STUDIES_FENCE_NAMES.forEach( - async (fenceName) => - await thunkAPI.dispatch( - fetchFenceStudies({ - fenceName, - userAcls: fenceConnection.fencesAcls[fenceName], - }), - ), - ); -}); - -const fetchFenceStudies = createAsyncThunk< - TFenceStudies, - { - fenceName: FENCE_NAMES; - userAcls: string[]; - }, - { rejectValue: string; state: RootState } ->( - 'fenceStudies/fetch/studies', - async (args, thunkAPI) => { - const { studies, error: authStudyError } = await getAuthStudyIdsAndCounts( - args.userAcls, - args.fenceName, - ); - - const { authorizedStudies, error: studiesCountError } = isEmpty(studies) - ? { authorizedStudies: [], error: undefined } - : await getStudiesCountByNameAndAcl(studies!); - - return handleThunkApiReponse({ - error: authStudyError || studiesCountError, - data: { - [args.fenceName]: { - authorizedStudies: authorizedStudies!, - }, - }, - reject: thunkAPI.rejectWithValue, - }); - }, - { - condition: (args, { getState }) => { - const { fenceStudies, fenceConnection } = getState(); - - const studies = fenceStudies.studies[args.fenceName]; - const hasNoAuthorizedStudies = isEmpty(studies) || isEmpty(studies.authorizedStudies); - const hasNotBeenDisconnected = [ - FENCE_CONNECTION_STATUSES.unknown, - FENCE_CONNECTION_STATUSES.connected, - ].includes(fenceConnection.connectionStatus[args.fenceName]); - - return ( - isEmpty(fenceStudies.studies[args.fenceName]) && - hasNoAuthorizedStudies && - hasNotBeenDisconnected - ); - }, - }, -); - -const getStudiesCountByNameAndAcl = async ( - studies: TFenceStudiesIdsAndCount, -): Promise<{ - error?: AxiosError; - authorizedStudies?: TFenceStudy[]; -}> => { - const studyIds = Object.keys(studies); - - const sqons = studyIds.reduce( - (obj, studyId) => ({ - ...obj, - [`${replaceDashByUnderscore(studyId)}_sqon`]: { - content: [ - { content: { field: 'participants.study_id', value: [studyId] }, op: TermOperators.in }, - ], - op: BooleanOperators.and, - }, - }), - {}, - ); - - const { data, error } = await ArrangerApi.graphqlRequest({ - query: ` - query StudyCountByNamesAndAcl(${studyIds.map( - (studyId) => `$${replaceDashByUnderscore(studyId)}_sqon: JSON`, - )}) { - file { - ${studyIds - .map( - (studyId) => ` - ${replaceDashByUnderscore(studyId)}: aggregations(filters: $${replaceDashByUnderscore( - studyId, - )}_sqon, aggregations_filter_themselves: true) { - acl { - buckets { - key - } - } - participants__study__study_name{ - buckets{ - key - doc_count - } - } - participants__study__study_code{ - buckets{ - key - } - } - } - `, - ) - .join('')} - } - } - `, - variables: sqons, - }); - - if (error) { - return { - authorizedStudies: undefined, - error, - }; - } - - const { - data: { file }, - } = data; - - return { - authorizedStudies: studyIds.map((id) => { - const studyId = replaceDashByUnderscore(id); - const agg = file[studyId]; - - return { - acl: agg['acl']['buckets'].map((a: any) => a.key).filter((b: any) => b.includes('.')), - studyShortName: agg['participants__study__study_name']['buckets'][0]['key'], - totalFiles: agg['participants__study__study_name']['buckets'][0]['doc_count'], - id, - code: agg['participants__study__study_code']['buckets'][0]['key'], - authorizedFiles: studies[id].authorizedFiles, - }; - }), - }; -}; - -const getAuthStudyIdsAndCounts = async ( - userAcls: string[], - fenceName: FENCE_NAMES, -): Promise<{ - error?: AxiosError; - studies?: TFenceStudiesIdsAndCount; -}> => { - const { data, error } = await ArrangerApi.graphqlRequest({ - query: ` - query AuthorizedStudyIdsAndCount($sqon: JSON) { - file { - aggregations(filters: $sqon, aggregations_filter_themselves: true, include_missing: false){ - participants__study__study_id { - buckets - { - key - doc_count - } - } - } - } - } - `, - variables: { - sqon: { - op: BooleanOperators.or, - content: [ - { - op: BooleanOperators.and, - content: [ - { op: TermOperators.in, content: { field: 'acl', value: userAcls } }, - { op: TermOperators.in, content: { field: 'repository', value: fenceName } }, - ], - }, - { - op: BooleanOperators.and, - content: [ - { - op: TermOperators.in, - content: { field: 'controlled_access', value: [FileAccessType.REGISTERED] }, - }, - ], - }, - ], - }, - }, - }); - - if (error) { - return { - studies: undefined, - error, - }; - } - - const { - data: { - file: { - aggregations: { - participants__study__study_id: { buckets }, - }, - }, - }, - } = data; - - return { - studies: buckets.reduce( - (obj: TFenceStudies, { key, doc_count }: { key: string; doc_count: number }) => ({ - ...obj, - [key]: { authorizedFiles: doc_count }, - }), - {}, - ), - }; -}; - -export const computeAllFencesAuthStudies = (fenceStudies: TFenceStudies) => { - if (isEmpty(fenceStudies)) { - return []; - } - - return Object.values(fenceStudies) - .map((x) => x.authorizedStudies) - .flat() - .reduce((xs: TFenceStudy[], x: TFenceStudy) => { - // remove duplicates - const sId = x.id; - return xs.some((s) => s.id === sId) ? xs : [...xs, { ...x }]; - }, []); -}; - -const replaceDashByUnderscore = (value: string) => value.replaceAll('-', ''); - -export { fetchFenceStudies, fetchAllFenceStudies }; diff --git a/src/store/fenceStudies/types.ts b/src/store/fenceStudies/types.ts deleted file mode 100644 index 54f54675c..000000000 --- a/src/store/fenceStudies/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { FENCE_NAMES } from 'common/fenceTypes'; - -export type initialState = { - studies: TFenceStudies; - loadingStudiesForFences: FENCE_NAMES[]; - fencesError: FENCE_NAMES[]; -}; - -export type TAclsByFenceName = { - [fenceName: string]: string[]; -}; - -export type TFenceStudy = { - acl: string[]; - studyShortName: string; - totalFiles: number; - id: string; - authorizedFiles: number; -}; - -export type TFenceStudies = { - [fenceName: string]: { - authorizedStudies: TFenceStudy[]; - }; -}; - -export type TFenceStudiesIdsAndCount = { - [fenceName: string]: { - authorizedFiles: number; - }; -}; diff --git a/src/store/fences/index.ts b/src/store/fences/index.ts index 5c982c3fa..d92ecbc3f 100644 --- a/src/store/fences/index.ts +++ b/src/store/fences/index.ts @@ -3,10 +3,15 @@ import { useDispatch, useSelector } from 'react-redux'; import { FENCE_NAMES } from 'common/fenceTypes'; -import { fencesAuthorizedStudiesSelector, fencesSelector } from './selector'; +import { + fencesAllAclsSelector, + fencesAtLeastOneAuthentificationConnectedSelector, + fencesAuthorizedStudiesSelector, + fencesSelector, +} from './selector'; import { fetchFenceAuthentificationStatus } from './thunks'; -export type { initialState as fencesInitialState } from './types'; +export type { InitialState as FencesInitialState } from './types'; export { default, FencesState } from './slice'; export const useFenceAuthentification = (fence: FENCE_NAMES) => { @@ -22,6 +27,11 @@ export const useFenceAuthentification = (fence: FENCE_NAMES) => { }; }; +export const useAtLeastOneFenceConnected = () => + useSelector(fencesAtLeastOneAuthentificationConnectedSelector); + +export const useAllFencesAcl = () => useSelector(fencesAllAclsSelector); + export const useFencesAuthorizedStudies = () => { const state = useSelector(fencesAuthorizedStudiesSelector); return { diff --git a/src/store/fences/selector.ts b/src/store/fences/selector.ts index e025b6294..dd4201bed 100644 --- a/src/store/fences/selector.ts +++ b/src/store/fences/selector.ts @@ -1,8 +1,32 @@ +import { FENCE_AUTHENTIFICATION_STATUS } from '@ferlab/ui/core/components/Widgets/AuthorizedStudies'; + +import { FENCE_NAMES } from 'common/fenceTypes'; import { RootState } from 'store/types'; -import { initialState } from './types'; +import { InitialState } from './types'; -export type FencesProps = initialState; +export type FencesProps = InitialState; export const fencesSelector = (state: RootState) => state.fences; export const fencesAuthorizedStudiesSelector = (state: RootState) => state.fences.authorizedStudies; +export const fencesAtLeastOneAuthentificationConnectedSelector = (state: RootState) => + Object.keys(FENCE_NAMES).some((fenceKey) => { + const key = fenceKey as FENCE_NAMES; + if (state.fences[key] === undefined) { + return false; + } + + return state.fences[key].status === FENCE_AUTHENTIFICATION_STATUS.connected; + }); + +export const fencesAllAclsSelector = (state: RootState) => { + const acls: string[] = []; + Object.keys(FENCE_NAMES).forEach((fenceKey) => { + const key = fenceKey as FENCE_NAMES; + if (state.fences[key].acl.length > 0) { + acls.concat(state.fences[key].acl); + } + }); + + return acls; +}; diff --git a/src/store/fences/slice.ts b/src/store/fences/slice.ts index 1f7722ec2..45c30d1a0 100644 --- a/src/store/fences/slice.ts +++ b/src/store/fences/slice.ts @@ -1,4 +1,4 @@ -import { FENCE_AUHTENTIFICATION_STATUS } from '@ferlab/ui/core/components/AuthorizedStudies'; +import { FENCE_AUTHENTIFICATION_STATUS } from '@ferlab/ui/core/components/Widgets/AuthorizedStudies'; import { createSlice } from '@reduxjs/toolkit'; import { FENCE_NAMES } from 'common/fenceTypes'; @@ -9,27 +9,20 @@ import { fetchAuthorizedStudies, fetchFenceAuthentificationStatus, } from './thunks'; -import { initialState } from './types'; +import { InitialState } from './types'; -export const FencesState: initialState = { +export const FencesState: InitialState = { [FENCE_NAMES.gen3]: { id: FENCE_NAMES.gen3, acl: [], - status: FENCE_AUHTENTIFICATION_STATUS.unknown, + status: FENCE_AUTHENTIFICATION_STATUS.unknown, loading: false, error: false, }, [FENCE_NAMES.dcf]: { id: FENCE_NAMES.dcf, acl: [], - status: FENCE_AUHTENTIFICATION_STATUS.unknown, - loading: false, - error: false, - }, - [FENCE_NAMES.cavatica]: { - id: FENCE_NAMES.dcf, - acl: [], - status: FENCE_AUHTENTIFICATION_STATUS.unknown, + status: FENCE_AUTHENTIFICATION_STATUS.unknown, loading: false, error: false, }, @@ -47,7 +40,7 @@ const fencesSlice = createSlice({ extraReducers: (builder) => { // Authentification API (open new window) builder.addCase(fenceOpenAuhentificationTab.pending, (state, action) => { - state[action.meta.arg].status = FENCE_AUHTENTIFICATION_STATUS.unknown; + state[action.meta.arg].status = FENCE_AUTHENTIFICATION_STATUS.unknown; state[action.meta.arg].error = false; state[action.meta.arg].loading = true; }); @@ -62,7 +55,7 @@ const fencesSlice = createSlice({ }); // Authentification status and acl builder.addCase(fetchFenceAuthentificationStatus.pending, (state, action) => { - state[action.meta.arg].status = FENCE_AUHTENTIFICATION_STATUS.unknown; + state[action.meta.arg].status = FENCE_AUTHENTIFICATION_STATUS.unknown; state[action.meta.arg].error = false; state[action.meta.arg].loading = true; }); @@ -91,17 +84,17 @@ const fencesSlice = createSlice({ }); // disconnection builder.addCase(fenceDisconnection.pending, (state, action) => { - state[action.meta.arg].status = FENCE_AUHTENTIFICATION_STATUS.unknown; + state[action.meta.arg].status = FENCE_AUTHENTIFICATION_STATUS.unknown; state[action.meta.arg].acl = []; state[action.meta.arg].loading = true; }); builder.addCase(fenceDisconnection.fulfilled, (state, action) => { - state[action.meta.arg].status = FENCE_AUHTENTIFICATION_STATUS.disconnected; + state[action.meta.arg].status = FENCE_AUTHENTIFICATION_STATUS.disconnected; state[action.meta.arg].loading = false; state.authorizedStudies.studies = []; }); builder.addCase(fenceDisconnection.rejected, (state, action) => { - state[action.meta.arg].status = FENCE_AUHTENTIFICATION_STATUS.disconnected; + state[action.meta.arg].status = FENCE_AUTHENTIFICATION_STATUS.disconnected; state[action.meta.arg].acl = []; state[action.meta.arg].loading = false; state[action.meta.arg].error = true; diff --git a/src/store/fences/thunks.ts b/src/store/fences/thunks.ts index 91cbe7ded..94bd1aba3 100644 --- a/src/store/fences/thunks.ts +++ b/src/store/fences/thunks.ts @@ -1,11 +1,11 @@ import { - FENCE_AUHTENTIFICATION_STATUS, + FENCE_AUTHENTIFICATION_STATUS, IAuthorizedStudy, -} from '@ferlab/ui/core/components/AuthorizedStudies'; -import { IFence } from '@ferlab/ui/core/components/AuthorizedStudies'; + IFence, +} from '@ferlab/ui/core/components/Widgets/AuthorizedStudies'; import { createAsyncThunk } from '@reduxjs/toolkit'; -import { FENCE_NAMES } from 'common/fenceTypes'; +import { ALL_FENCE_NAMES, FENCE_NAMES } from 'common/fenceTypes'; import { FenceApi } from 'services/api/fence'; import { RootState } from 'store/types'; import { handleThunkApiReponse } from 'store/utils'; @@ -54,9 +54,19 @@ export const fetchAuthorizedStudies = createAsyncThunk< }); }); +export const fetchAllFencesAuthentificationStatus = createAsyncThunk< + void, + void, + { state: RootState } +>('fence/check/all/auth/statue', async (_, thunkAPI) => { + ALL_FENCE_NAMES.forEach((fence) => { + thunkAPI.dispatch(fetchFenceAuthentificationStatus(fence)); + }); +}); + export const fetchFenceAuthentificationStatus = createAsyncThunk< { - status: FENCE_AUHTENTIFICATION_STATUS; + status: FENCE_AUTHENTIFICATION_STATUS; acl: string[]; }, FENCE_NAMES, @@ -74,8 +84,8 @@ export const fetchFenceAuthentificationStatus = createAsyncThunk< error, data: { status: data?.authenticated - ? FENCE_AUHTENTIFICATION_STATUS.connected - : FENCE_AUHTENTIFICATION_STATUS.disconnected, + ? FENCE_AUTHENTIFICATION_STATUS.connected + : FENCE_AUTHENTIFICATION_STATUS.disconnected, acl, }, reject: thunkAPI.rejectWithValue, @@ -84,7 +94,7 @@ export const fetchFenceAuthentificationStatus = createAsyncThunk< export const fenceOpenAuhentificationTab = createAsyncThunk< { - status: FENCE_AUHTENTIFICATION_STATUS; + status: FENCE_AUTHENTIFICATION_STATUS; acl: string[]; }, FENCE_NAMES, @@ -92,13 +102,7 @@ export const fenceOpenAuhentificationTab = createAsyncThunk< state: RootState; } >('fence/connection', async (fence, thunkAPI) => { - const { fenceConnection } = thunkAPI.getState(); - let fenceInfo = fenceConnection.fencesInfo[fence]; - - if (!fenceInfo) { - const { data } = await FenceApi.fetchInfo(fence); - fenceInfo = data; - } + const { data: fenceInfo } = await FenceApi.fetchInfo(fence); const authWindow = window.open(fenceInfo?.authorize_uri)!; @@ -118,8 +122,8 @@ export const fenceOpenAuhentificationTab = createAsyncThunk< resolve({ status: data?.authenticated - ? FENCE_AUHTENTIFICATION_STATUS.connected - : FENCE_AUHTENTIFICATION_STATUS.disconnected, + ? FENCE_AUTHENTIFICATION_STATUS.connected + : FENCE_AUTHENTIFICATION_STATUS.disconnected, acl, }); } else { diff --git a/src/store/fences/types.ts b/src/store/fences/types.ts index 5a289db71..ec1bbf5e6 100644 --- a/src/store/fences/types.ts +++ b/src/store/fences/types.ts @@ -1,11 +1,9 @@ -import { IAuthorizedStudies } from '@ferlab/ui/core/components/AuthorizedStudies'; -import { IFence } from '@ferlab/ui/core/components/AuthorizedStudies'; +import { IAuthorizedStudies, IFence } from '@ferlab/ui/core/components/Widgets/AuthorizedStudies'; -export type initialState = { +export type InitialState = { authorizedStudies: IAuthorizedStudies; gen3: IFence; dcf: IFence; - cavatica: IFence; }; export interface IAuthorizedStudiesFetchParams { diff --git a/src/store/index.tsx b/src/store/index.tsx index 5f878dc12..08749693f 100644 --- a/src/store/index.tsx +++ b/src/store/index.tsx @@ -6,12 +6,10 @@ import { persistReducer, persistStore } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; import createFilter from 'redux-persist-transform-filter'; -import FenceCavaticaReducer from 'store/fenceCavatica'; -import FenceConnectionReducer from 'store/fenceConnection'; import FencesReducer from 'store/fences'; -import FenceStudiesReducer from 'store/fenceStudies'; import GlobalReducer from 'store/global'; import NotebookReducer from 'store/notebook'; +import PassportReducer from 'store/passport'; import PersonaReducer from 'store/persona'; import RemoteReducer from 'store/remote'; import ReportReducer from 'store/report'; @@ -34,13 +32,11 @@ export const rootReducer = combineReducers({ fences: FencesReducer, persona: PersonaReducer, notebook: NotebookReducer, + passport: PassportReducer, user: UserReducer, report: ReportReducer, - fenceConnection: FenceConnectionReducer, - fenceStudies: FenceStudiesReducer, savedFilter: SavedFilterReducer, savedSet: SavedSetReducer, - fenceCavatica: FenceCavaticaReducer, remote: RemoteReducer, }); diff --git a/src/store/passport/index.ts b/src/store/passport/index.ts new file mode 100644 index 000000000..dfd23d664 --- /dev/null +++ b/src/store/passport/index.ts @@ -0,0 +1,31 @@ +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { PASSPORT_AUTHENTIFICATION_STATUS } from '@ferlab/ui/core/components/Widgets/Cavatica/type'; + +import { passportSelector } from './selector'; +import { + fetchCavaticaAuthentificationStatus, + fetchCavaticaBillingGroups, + fetchCavaticaProjects, +} from './thunks'; + +export type { InitialState as PassportInitialState } from './type'; +export { default, passportState } from './slice'; + +export const useCavaticaPassport = () => { + const state = useSelector(passportSelector); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchCavaticaAuthentificationStatus()); + }, []); + + useEffect(() => { + if (state.cavatica.authentification.status === PASSPORT_AUTHENTIFICATION_STATUS.connected) { + dispatch(fetchCavaticaProjects()); + dispatch(fetchCavaticaBillingGroups()); + } + }, [state.cavatica.authentification.status]); + + return state.cavatica; +}; diff --git a/src/store/passport/selector.ts b/src/store/passport/selector.ts new file mode 100644 index 000000000..55002c0af --- /dev/null +++ b/src/store/passport/selector.ts @@ -0,0 +1,4 @@ +import { RootState } from 'store/types'; + +export const passportSelector = (state: RootState) => state.passport; +export const passportCavaticaSelector = (state: RootState) => state.passport.cavatica; diff --git a/src/store/passport/slice.ts b/src/store/passport/slice.ts new file mode 100644 index 000000000..517bfc5aa --- /dev/null +++ b/src/store/passport/slice.ts @@ -0,0 +1,147 @@ +import { CAVATICA_API_ERROR_TYPE } from '@ferlab/ui/core/components/Widgets/Cavatica'; +import { + CAVATICA_ANALYSE_STATUS, + PASSPORT_AUTHENTIFICATION_STATUS, +} from '@ferlab/ui/core/components/Widgets/Cavatica/type'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { + beginCavaticaAnalyse, + createCavaticaProjet, + disconnectCavaticaPassport, + fetchCavaticaAuthentificationStatus, + fetchCavaticaBillingGroups, + fetchCavaticaProjects, +} from './thunks'; +import { InitialState } from './type'; + +export const passportState: InitialState = { + cavatica: { + authentification: { + status: PASSPORT_AUTHENTIFICATION_STATUS.unknown, + error: false, + loading: false, + }, + projects: { + data: [], + loading: false, + error: false, + }, + billingGroups: { + data: [], + loading: false, + error: false, + }, + bulkImportData: { + loading: false, + status: CAVATICA_ANALYSE_STATUS.unknow, + files: [], + authorizedFiles: [], + }, + }, +}; + +const passportSlice = createSlice({ + name: 'passport', + initialState: passportState, + reducers: { + resetCavaticaProjectsError: (state) => { + state.cavatica.projects.error = false; + }, + resetCavaticaBillingsGroupError: (state) => { + state.cavatica.billingGroups.error = false; + }, + setCavaticaBulkImportDataStatus: (state, action: PayloadAction) => { + state.cavatica.bulkImportData.status = action.payload; + }, + }, + extraReducers: (builder) => { + // Cavatica + // AUTHENTIFICATION + builder.addCase(fetchCavaticaAuthentificationStatus.pending, (state, action) => { + state.cavatica.projects.error = false; + state.cavatica.billingGroups.error = false; + state.cavatica.bulkImportData.status = CAVATICA_ANALYSE_STATUS.unknow; + state.cavatica.authentification.loading = true; + state.cavatica.authentification.status = PASSPORT_AUTHENTIFICATION_STATUS.unknown; + }); + builder.addCase(fetchCavaticaAuthentificationStatus.fulfilled, (state, action) => { + state.cavatica.authentification.loading = false; + state.cavatica.authentification.status = action.payload.status; + }); + builder.addCase(fetchCavaticaAuthentificationStatus.rejected, (state, action) => { + state.cavatica.authentification.loading = false; + state.cavatica.authentification.error = true; + }); + // DISCONNECTION + builder.addCase(disconnectCavaticaPassport.pending, (state, action) => { + state.cavatica.authentification.loading = true; + state.cavatica.authentification.status = PASSPORT_AUTHENTIFICATION_STATUS.unknown; + }); + builder.addCase(disconnectCavaticaPassport.fulfilled, (state, action) => { + state.cavatica.authentification.loading = false; + state.cavatica.authentification.status = PASSPORT_AUTHENTIFICATION_STATUS.disconnected; + }); + builder.addCase(disconnectCavaticaPassport.rejected, (state, action) => { + state.cavatica.authentification.loading = false; + state.cavatica.authentification.error = true; + }); + // FETCH PROJECT + builder.addCase(fetchCavaticaProjects.pending, (state, action) => { + state.cavatica.projects.loading = true; + }); + builder.addCase(fetchCavaticaProjects.fulfilled, (state, action) => { + state.cavatica.projects.loading = false; + state.cavatica.projects.data = action.payload; + }); + builder.addCase(fetchCavaticaProjects.rejected, (state, action) => { + state.cavatica.projects.loading = false; + state.cavatica.projects.error = CAVATICA_API_ERROR_TYPE.fetch; + }); + // BILLINGS GROUPS + builder.addCase(fetchCavaticaBillingGroups.pending, (state, action) => { + state.cavatica.billingGroups.loading = true; + }); + builder.addCase(fetchCavaticaBillingGroups.fulfilled, (state, action) => { + state.cavatica.billingGroups.loading = false; + state.cavatica.billingGroups.data = action.payload; + }); + builder.addCase(fetchCavaticaBillingGroups.rejected, (state, action) => { + state.cavatica.billingGroups.loading = false; + state.cavatica.billingGroups.error = action.payload; + }); + // CREATE PROJECT + builder.addCase(createCavaticaProjet.pending, (state, action) => { + state.cavatica.projects.loading = true; + }); + builder.addCase(createCavaticaProjet.fulfilled, (state, action) => { + state.cavatica.projects.loading = false; + }); + builder.addCase(createCavaticaProjet.rejected, (state, action) => { + state.cavatica.projects.loading = false; + state.cavatica.projects.error = CAVATICA_API_ERROR_TYPE.create; + }); + // ANALYSE PROJECT + builder.addCase(beginCavaticaAnalyse.pending, (state, action) => { + state.cavatica.bulkImportData.loading = true; + state.cavatica.bulkImportData.status = CAVATICA_ANALYSE_STATUS.unknow; + }); + builder.addCase(beginCavaticaAnalyse.fulfilled, (state, action) => { + state.cavatica.bulkImportData.loading = false; + state.cavatica.bulkImportData.status = CAVATICA_ANALYSE_STATUS.analyzed; + state.cavatica.bulkImportData.files = action.payload.files; + state.cavatica.bulkImportData.authorizedFiles = action.payload.authorizedFiles; + }); + builder.addCase(beginCavaticaAnalyse.rejected, (state, action) => { + state.cavatica.bulkImportData.loading = false; + if (action.payload) { + state.cavatica.bulkImportData.status = action.payload as CAVATICA_ANALYSE_STATUS; + } else { + state.cavatica.bulkImportData.status = CAVATICA_ANALYSE_STATUS.generic_error; + } + }); + }, +}); + +export const passportActions = passportSlice.actions; +export default passportSlice.reducer; diff --git a/src/store/passport/thunks.ts b/src/store/passport/thunks.ts new file mode 100644 index 000000000..2f22aed80 --- /dev/null +++ b/src/store/passport/thunks.ts @@ -0,0 +1,286 @@ +import intl from 'react-intl-universal'; +import { FENCE_AUTHENTIFICATION_STATUS } from '@ferlab/ui/core/components/Widgets/AuthorizedStudies'; +import { PASSPORT } from '@ferlab/ui/core/components/Widgets/Cavatica'; +import { + CAVATICA_ANALYSE_STATUS, + PASSPORT_AUTHENTIFICATION_STATUS, +} from '@ferlab/ui/core/components/Widgets/Cavatica/type'; +import { BooleanOperators } from '@ferlab/ui/core/data/sqon/operators'; +import { ISqonGroupFilter } from '@ferlab/ui/core/data/sqon/types'; +import { termToSqon } from '@ferlab/ui/core/data/sqon/utils'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { IFileEntity, IFileResultTree } from 'graphql/files/models'; +import { SEARCH_FILES_QUERY } from 'graphql/files/queries'; +import { hydrateResults } from 'graphql/models'; +import { isEmpty } from 'lodash'; +import { CAVATICA_FILE_BATCH_SIZE } from 'views/DataExploration/utils/constant'; + +import { FENCE_NAMES } from 'common/fenceTypes'; +import { ArrangerApi } from 'services/api/arranger'; +import { CavaticaApi } from 'services/api/cavatica'; +import { + ICavaticaBillingGroup, + ICavaticaCreateProjectBody, + ICavaticaProject, +} from 'services/api/cavatica/models'; +import { FenceApi } from 'services/api/fence'; +import { globalActions } from 'store/global'; +import { RootState } from 'store/types'; +import { handleThunkApiReponse } from 'store/utils'; +import { userHasAccessToFile } from 'utils/dataFiles'; + +const TEN_MINUTES_IN_MS = 1000 * 60 * 10; + +// TODO: Still using the legacy fence authentification, will be changed in the futur for a passport +export const fetchCavaticaAuthentificationStatus = createAsyncThunk< + { + status: PASSPORT_AUTHENTIFICATION_STATUS; + acl: string[]; + }, + void, + { state: RootState } +>('passport/cavatica/auth/status', async (_, thunkAPI) => { + const { data, error } = await FenceApi.isAuthenticated(PASSPORT.cavatica); + + let acl: string[] = []; + if (data?.authenticated) { + const { data } = await FenceApi.fetchAcls(PASSPORT.cavatica); + acl = data?.acl || []; + } + + return handleThunkApiReponse({ + error, + data: { + status: data?.authenticated + ? PASSPORT_AUTHENTIFICATION_STATUS.connected + : PASSPORT_AUTHENTIFICATION_STATUS.disconnected, + acl, + }, + reject: thunkAPI.rejectWithValue, + }); +}); + +// TODO: Still using the legacy fence authentification, will be changed in the futur for a passport +export const disconnectCavaticaPassport = createAsyncThunk( + 'passport/cavatica/auth/disconnection', + async (_, thunkAPI) => { + const { data, error } = await FenceApi.disconnect(PASSPORT.cavatica); + + return handleThunkApiReponse({ + error, + data, + reject: thunkAPI.rejectWithValue, + }); + }, +); + +// TODO: Still using the legacy fence authentification, will be changed in the futur for a passport +export const connectCavaticaPassport = createAsyncThunk< + void, + void, + { + state: RootState; + } +>('passport/cavatica/auth/connection', async (_, thunkAPI) => { + const { data } = await FenceApi.fetchInfo(PASSPORT.cavatica); + const authWindow = window.open(data?.authorize_uri)!; + + return new Promise((resolve, reject) => { + const interval = setInterval(async () => { + if (authWindow.closed) { + await thunkAPI.dispatch(fetchCavaticaAuthentificationStatus()); + const { + passport: { + cavatica: { + authentification: { status }, + }, + }, + } = thunkAPI.getState(); + + if (status === PASSPORT_AUTHENTIFICATION_STATUS.connected) { + clearInterval(interval); + resolve(); + } else { + clearInterval(interval); + reject('failed authenticating'); + } + } + }, 1000); + setTimeout(() => { + clearInterval(interval); + reject('nothing'); + }, TEN_MINUTES_IN_MS); + }); +}); + +export const fetchCavaticaProjects = createAsyncThunk< + ICavaticaProject[], + void, + { rejectValue: string; state: RootState } +>( + 'passport/cavatica/fetch/projects', + async (_, thunkAPI) => { + const { data, error } = await CavaticaApi.fetchProjects(); + const projects = data?.items || []; + + if (error) { + return thunkAPI.rejectWithValue(error.message); + } + + const projectList = await Promise.all( + (projects || []).map(async (project) => { + const [memberResponse] = await Promise.all([CavaticaApi.fetchProjetMembers(project.id)]); + + return { ...project, memberCount: memberResponse.data?.items.length || 0 }; + }), + ); + + return projectList.sort((p1, p2) => + new Date(p1.modified_on) < new Date(p2.modified_on) ? 1 : -1, + ); + }, + { + condition: (_, { getState }) => { + const { passport } = getState(); + return isEmpty(passport.cavatica.projects.data); + }, + }, +); + +export const fetchCavaticaBillingGroups = createAsyncThunk< + ICavaticaBillingGroup[], + void, + { rejectValue: string; state: RootState } +>( + 'passport/cavatica/fetch/billingGroups', + async (_, thunkAPI) => { + const { data, error } = await CavaticaApi.fetchBillingGroups(); + + return handleThunkApiReponse({ + error, + data: data?.items || [], + reject: thunkAPI.rejectWithValue, + onError: () => + thunkAPI.dispatch( + globalActions.displayNotification({ + type: 'error', + message: intl.get('api.cavatica.error.title'), + description: intl.get('api.cavatica.error.billingGroups.fetch'), + }), + ), + }); + }, + { + condition: (_, { getState }) => { + const { passport } = getState(); + + return isEmpty(passport.cavatica.billingGroups.data); + }, + }, +); + +export const createCavaticaProjet = createAsyncThunk< + { + newProject: ICavaticaProject; + }, + { + body: ICavaticaCreateProjectBody; + }, + { rejectValue: string; state: RootState } +>('passport/cavatica/create/project', async (args, thunkAPI) => { + const { data, error } = await CavaticaApi.createProject(args.body); + + return handleThunkApiReponse({ + error, + reject: thunkAPI.rejectWithValue, + onSuccess: () => { + thunkAPI.dispatch( + globalActions.displayNotification({ + type: 'success', + message: intl.get('api.cavatica.success.title'), + description: intl.get('api.cavatica.success.projects.create'), + }), + ); + }, + data: { + newProject: data!, + }, + }); +}); + +export const beginCavaticaAnalyse = createAsyncThunk< + { + files: IFileEntity[]; + authorizedFiles: IFileEntity[]; + }, + { + sqon: ISqonGroupFilter; + fileIds: string[]; + }, + { rejectValue: string; state: RootState } +>('passport/cavatica/begin/analyse', async (args, thunkAPI) => { + if (args.fileIds.length > CAVATICA_FILE_BATCH_SIZE) { + return thunkAPI.rejectWithValue(CAVATICA_ANALYSE_STATUS.upload_limit_reached); + } + + const { fences, passport } = thunkAPI.getState(); + const acls: string[] = []; + + for (const fenceKey in fences) { + const key = fenceKey as FENCE_NAMES; + if (fences[key].acl !== undefined) { + acls.concat(fences[key].acl); + } + } + + const sqon: ISqonGroupFilter = { + op: BooleanOperators.and, + content: [args.sqon], + }; + + if (args.fileIds.length > 0) { + sqon.content = [ + ...sqon.content, + termToSqon({ + field: 'file_id', + value: args.fileIds, + }), + ]; + } + + const { data, error } = await ArrangerApi.graphqlRequest<{ data: IFileResultTree }>({ + query: SEARCH_FILES_QUERY.loc?.source.body, + variables: { + sqon, + first: CAVATICA_FILE_BATCH_SIZE, + }, + }); + + const files = hydrateResults(data?.data?.file?.hits?.edges || []); + + const authorizedFiles = files.filter((file) => + userHasAccessToFile( + file, + acls, + passport.cavatica.authentification.status === PASSPORT_AUTHENTIFICATION_STATUS.connected, + fences.gen3.status === FENCE_AUTHENTIFICATION_STATUS.connected, + ), + ); + + if (!authorizedFiles.length) { + return thunkAPI.rejectWithValue(CAVATICA_ANALYSE_STATUS.unauthorize); + } + + if (passport.cavatica.authentification.status === PASSPORT_AUTHENTIFICATION_STATUS.connected) { + thunkAPI.dispatch(fetchCavaticaProjects()); + } + + return handleThunkApiReponse({ + error, + reject: thunkAPI.rejectWithValue, + data: { + files, + authorizedFiles, + }, + }); +}); diff --git a/src/store/passport/type.ts b/src/store/passport/type.ts new file mode 100644 index 000000000..3de002b12 --- /dev/null +++ b/src/store/passport/type.ts @@ -0,0 +1,33 @@ +import { + CAVATICA_ANALYSE_STATUS, + PASSPORT_AUTHENTIFICATION_STATUS, +} from '@ferlab/ui/core/components/Widgets/Cavatica/type'; +import { IFileEntity } from 'graphql/files/models'; + +import { ICavaticaBillingGroup, ICavaticaProject } from 'services/api/cavatica/models'; + +export type InitialState = { + cavatica: { + authentification: { + status: PASSPORT_AUTHENTIFICATION_STATUS; + error: boolean; + loading: boolean; + }; + projects: { + data: ICavaticaProject[]; + loading: boolean; + error?: any; + }; + billingGroups: { + data: ICavaticaBillingGroup[]; + loading: boolean; + error?: any; + }; + bulkImportData: { + loading: boolean; + files: IFileEntity[]; + authorizedFiles: IFileEntity[]; + status: CAVATICA_ANALYSE_STATUS; + }; + }; +}; diff --git a/src/store/report/thunks.tsx b/src/store/report/thunks.tsx index 2bcb9ee38..0553f89c1 100644 --- a/src/store/report/thunks.tsx +++ b/src/store/report/thunks.tsx @@ -20,7 +20,7 @@ import { getColumnStateQuery } from '../../graphql/reports/queries'; import { TFetchTSVArgs } from './types'; -export const SUPPORT_EMAIL = 'support@includedcc.org'; +export const SUPPORT_EMAIL = 'support@kidsfirstdrc.org'; const showErrorReportNotif = (thunkApi: any) => thunkApi.dispatch( diff --git a/src/store/types.ts b/src/store/types.ts index 659ffc1ab..f09950025 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -1,12 +1,10 @@ -import { FenceConnectionInitialState } from 'store/fenceConnection'; import { GlobalInitialState } from 'store/global'; import { PersonaInitialState } from 'store/persona'; import { UserInitialState } from 'store/user'; -import { FenceCavaticaInitialState } from './fenceCavatica'; -import { fencesInitialState } from './fences'; -import { fenceStudiesInitialState } from './fenceStudies'; +import { FencesInitialState } from './fences'; import { NotebookInitialState } from './notebook'; +import { PassportInitialState } from './passport'; import { RemoteInitialState } from './remote'; import { ReportInitialState } from './report'; import { SavedFilterInitialState } from './savedFilter'; @@ -18,11 +16,9 @@ export type RootState = { persona: PersonaInitialState; notebook: NotebookInitialState; report: ReportInitialState; - fenceConnection: FenceConnectionInitialState; - fenceStudies: fenceStudiesInitialState; - fences: fencesInitialState; + fences: FencesInitialState; savedFilter: SavedFilterInitialState; savedSet: SavedSetInitialState; - fenceCavatica: FenceCavaticaInitialState; remote: RemoteInitialState; + passport: PassportInitialState; }; diff --git a/src/tests/setupTests.tsx b/src/tests/setupTests.tsx index 07a1a7c91..931a53bb2 100644 --- a/src/tests/setupTests.tsx +++ b/src/tests/setupTests.tsx @@ -15,13 +15,12 @@ import enUS from 'antd/lib/locale/en_US'; import locales from 'locales'; import { LANG } from 'common/constants'; -import { FenceCavaticaState } from 'store/fenceCavatica'; -import { FenceConnectionState } from 'store/fenceConnection'; -import { FenceStudiesState } from 'store/fenceStudies'; +import { FencesState } from 'store/fences'; import { GlobalState } from 'store/global/slice'; import type { AppStore } from 'store/index'; import { store as setupStore } from 'store/index'; import { NotebookState } from 'store/notebook/slice'; +import { passportState } from 'store/passport'; import { PersonaState } from 'store/persona'; import { RemoteState } from 'store/remote'; import { ReportState } from 'store/report'; @@ -41,11 +40,10 @@ export const defaultPreloadedState = { persona: PersonaState, notebook: NotebookState, report: ReportState, - fenceConnection: FenceConnectionState, - fenceStudies: FenceStudiesState, savedFilter: SavedFilterState, savedSet: SavedSetState, - fenceCavatica: FenceCavaticaState, + fences: FencesState, + passport: passportState, remote: RemoteState, }; diff --git a/src/utils/routes.ts b/src/utils/routes.ts index 29f3bb781..23aad4249 100644 --- a/src/utils/routes.ts +++ b/src/utils/routes.ts @@ -25,7 +25,7 @@ export enum STATIC_ROUTES { DCF_FENCE_REDIRECT = '/dcf_redirect', GEN3_FENCE_REDIRECT = '/gen3_redirect', - CAVATICA_FENCE_REDIRECT = '/cavatica_redirect', + CAVATICA_PASSPORT_REDIRECT = '/cavatica_redirect', FAKE_STORYBOOK = '/v2/temp/fake/storybook', } diff --git a/src/views/Dashboard/components/DashboardCards/AuthorizedStudies/index.test.tsx b/src/views/Dashboard/components/DashboardCards/AuthorizedStudies/index.test.tsx index 49f15fba6..908990ad7 100644 --- a/src/views/Dashboard/components/DashboardCards/AuthorizedStudies/index.test.tsx +++ b/src/views/Dashboard/components/DashboardCards/AuthorizedStudies/index.test.tsx @@ -1,5 +1,4 @@ import { BrowserRouter } from 'react-router-dom'; -import { FENCE_AUHTENTIFICATION_STATUS } from '@ferlab/ui/core/components/AuthorizedStudies'; import { screen } from '@testing-library/react'; import { renderWithProviders } from 'tests/setupTests'; diff --git a/src/views/Dashboard/components/DashboardCards/AuthorizedStudies/index.tsx b/src/views/Dashboard/components/DashboardCards/AuthorizedStudies/index.tsx index e3d70ee90..fcecf7bf1 100644 --- a/src/views/Dashboard/components/DashboardCards/AuthorizedStudies/index.tsx +++ b/src/views/Dashboard/components/DashboardCards/AuthorizedStudies/index.tsx @@ -2,9 +2,9 @@ import { useEffect } from 'react'; import intl from 'react-intl-universal'; import { useDispatch } from 'react-redux'; import AuthorizedStudiesWidget, { + FENCE_AUTHENTIFICATION_STATUS, IFenceService, -} from '@ferlab/ui/core/components/AuthorizedStudies'; -import { FENCE_AUHTENTIFICATION_STATUS } from '@ferlab/ui/core/components/AuthorizedStudies'; +} from '@ferlab/ui/core/components/Widgets/AuthorizedStudies'; import { INDEXES } from 'graphql/constants'; import { DATA_EXPLORATION_QB_ID } from 'views/DataExploration/utils/constant'; @@ -54,7 +54,7 @@ const AuthorizedStudies = ({ id, className = '' }: DashboardCardProps) => { ]; useEffect(() => { - if (!fences.some(({ status }) => status === FENCE_AUHTENTIFICATION_STATUS.connected)) { + if (!fences.some(({ status }) => status === FENCE_AUTHENTIFICATION_STATUS.connected)) { return; } @@ -80,7 +80,6 @@ const AuthorizedStudies = ({ id, className = '' }: DashboardCardProps) => { count: authorizedStudies.studies.length, }), connectedNotice: intl.get('screen.dashboard.cards.authorizedStudies.connectedNotice'), - disconnectedNotice: intl.get('screen.dashboard.cards.authorizedStudies.disconnectedNotice'), manageConnections: intl.get('screen.dashboard.cards.authorizedStudies.manageConnections'), noAvailableStudies: intl.get('screen.dashboard.cards.authorizedStudies.noAvailableStudies'), authentification: { diff --git a/src/views/Dashboard/components/DashboardCards/Cavatica/ListItem/index.module.scss b/src/views/Dashboard/components/DashboardCards/Cavatica/ListItem/index.module.scss deleted file mode 100644 index fbe4ef7df..000000000 --- a/src/views/Dashboard/components/DashboardCards/Cavatica/ListItem/index.module.scss +++ /dev/null @@ -1,40 +0,0 @@ -@import 'src/style/themes/kids-first/colors'; - -.CavaticaListItem { - .itemMeta { - margin-bottom: 0px; - - *[class$='-meta-title'] { - font-size: 14px; - } - } - - .externalIcon { - margin-left: 8px; - visibility: hidden; - } - - &:hover { - .projectLink { - text-decoration: underline; - &:hover { - color: $gray-8; - text-decoration: none; - } - - .externalIcon { - visibility: visible; - } - } - } - - .members { - font-size: 12px; - color: $gray-7; - } - - .teamIcon { - font-size: 16px; - display: flex; - } -} diff --git a/src/views/Dashboard/components/DashboardCards/Cavatica/ListItem/index.tsx b/src/views/Dashboard/components/DashboardCards/Cavatica/ListItem/index.tsx deleted file mode 100644 index 510480708..000000000 --- a/src/views/Dashboard/components/DashboardCards/Cavatica/ListItem/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import cx from 'classnames'; -import { List, Space, Typography } from 'antd'; -import intl from 'react-intl-universal'; -import { TeamOutlined } from '@ant-design/icons'; -import EnvironmentVariables from 'helpers/EnvVariables'; -import { TCavaticaProjectWithMembers } from 'store/fenceCavatica/types'; - -import styles from './index.module.scss'; -import ExternalLink from '@ferlab/ui/core/components/ExternalLink'; - -interface OwnProps { - id: any; - data: TCavaticaProjectWithMembers; -} - -const USER_BASE_URL = EnvironmentVariables.configFor('CAVATICA_USER_BASE_URL'); -const { Text } = Typography; - -const CavaticaListItem = ({ id, data }: OwnProps) => { - return ( - - - {data.name} - - } - className={styles.itemMeta} - /> - - - - {intl.get('screen.dashboard.cards.cavatica.membersCount', { - count: data.memberCount, - })} - - - - ); -}; - -export default CavaticaListItem; diff --git a/src/views/Dashboard/components/DashboardCards/Cavatica/index.module.scss b/src/views/Dashboard/components/DashboardCards/Cavatica/index.module.scss deleted file mode 100644 index 19e8b6548..000000000 --- a/src/views/Dashboard/components/DashboardCards/Cavatica/index.module.scss +++ /dev/null @@ -1,39 +0,0 @@ -@import 'src/style/themes/kids-first/colors'; - -.cavaticaWrapper { - height: 100%; - display: flex; - flex-direction: column; - - .cavaticaProjectsList { - overflow: auto; - height: 100%; - } - - .authenticatedHeader { - margin-bottom: 8px; - - .disconnectBtn { - padding: 0; - font-size: 12px; - } - - .notice { - font-size: 12px; - line-height: 10px; - } - } - - .customFooter { - margin-top: 12px; - - [ant-click-animating-without-extra-node='true']::after { - display: none; - } - } -} - -.cavaticaInfoIcon { - color: $gray-7; - font-size: 18px; -} diff --git a/src/views/Dashboard/components/DashboardCards/Cavatica/index.tsx b/src/views/Dashboard/components/DashboardCards/Cavatica/index.tsx index 3aad83449..4e8de26c7 100644 --- a/src/views/Dashboard/components/DashboardCards/Cavatica/index.tsx +++ b/src/views/Dashboard/components/DashboardCards/Cavatica/index.tsx @@ -1,140 +1,105 @@ -import { useEffect } from 'react'; import intl from 'react-intl-universal'; import { useDispatch } from 'react-redux'; -import { DisconnectOutlined, PlusOutlined, SafetyOutlined } from '@ant-design/icons'; -import Empty from '@ferlab/ui/core/components/Empty'; -import GridCard from '@ferlab/ui/core/view/v2/GridCard'; -import { Button, List, Space } from 'antd'; -import Text from 'antd/lib/typography/Text'; -import CardConnectPlaceholder from 'views/Dashboard/components/CardConnectPlaceholder'; -import CardHeader from 'views/Dashboard/components/CardHeader'; -import { DashboardCardProps } from 'views/Dashboard/components/DashboardCards'; +import CavaticaWidget from '@ferlab/ui/core/components/Widgets/Cavatica'; +import EnvironmentVariables from 'helpers/EnvVariables'; -import { FENCE_NAMES } from 'common/fenceTypes'; -import CreateProjectModal from 'components/Cavatica/CreateProjectModal'; -import CavaticaIcon from 'components/Icons/CavaticaIcon'; -import PopoverContentLink from 'components/uiKit/PopoverContentLink'; -import { useFenceCavatica } from 'store/fenceCavatica'; -import { fenceCavaticaActions } from 'store/fenceCavatica/slice'; -import { fetchAllProjects } from 'store/fenceCavatica/thunks'; -import { TCavaticaProjectWithMembers } from 'store/fenceCavatica/types'; -import { connectToFence, disconnectFromFence } from 'store/fenceConnection/thunks'; +import { ICavaticaCreateProjectBody } from 'services/api/cavatica/models'; +import { useCavaticaPassport } from 'store/passport'; +import { passportActions } from 'store/passport/slice'; +import { + connectCavaticaPassport, + createCavaticaProjet, + disconnectCavaticaPassport, +} from 'store/passport/thunks'; +import { SUPPORT_EMAIL } from 'store/report/thunks'; -import CavaticaListItem from './ListItem'; +import { DashboardCardProps } from '..'; -import styles from './index.module.scss'; +const USER_BASE_URL = EnvironmentVariables.configFor('CAVATICA_USER_BASE_URL'); const Cavatica = ({ id, className = '' }: DashboardCardProps) => { const dispatch = useDispatch(); - const { projects, isLoading, isConnected, connectionLoading } = useFenceCavatica(); - - useEffect(() => { - if (isConnected) { - dispatch(fetchAllProjects()); - } - // eslint-disable-next-line - }, [isConnected]); + const cavatica = useCavaticaPassport(); return ( - <> - - - {intl.get('screen.dashboard.cards.cavatica.infoPopover.content')}{' '} - - - - ), - }} - withHandle - /> - } - content={ -
- {isConnected && ( - - - - - {intl.get('screen.dashboard.cards.cavatica.connectedNotice')} - - - - - )} - - className={styles.cavaticaProjectsList} - bordered - itemLayout="vertical" - loading={isLoading || connectionLoading} - locale={{ - emptyText: isConnected ? ( - } - size="small" - onClick={() => dispatch(fenceCavaticaActions.beginCreateProject())} - > - {intl.get('screen.dashboard.cards.cavatica.createNewProject')} - - } - /> - ) : ( - } - description={intl.get('screen.dashboard.cards.cavatica.disconnectedNotice')} - btnProps={{ - onClick: () => dispatch(connectToFence(FENCE_NAMES.cavatica)), - }} - /> - ), - }} - dataSource={isConnected ? projects : []} - renderItem={(item) => } - > - {(isConnected ? projects : []).length > 0 && ( -
- -
- )} -
- } - /> - {isConnected && } - + { + dispatch(passportActions.resetCavaticaBillingsGroupError()); + dispatch(passportActions.resetCavaticaProjectsError()); + }} + createProjectModalProps={{ + handleSubmit: (values: ICavaticaCreateProjectBody) => { + dispatch( + createCavaticaProjet({ + body: values, + }), + ); + }, + }} + cavaticaUrl={USER_BASE_URL} + className={className} + handleDisconnection={() => { + dispatch(disconnectCavaticaPassport()); + }} + handleConnection={() => { + dispatch(connectCavaticaPassport()); + }} + id={id} + dictionary={{ + title: intl.get('screen.dashboard.cards.cavatica.title'), + connectedNotice: intl.get('screen.dashboard.cards.cavatica.connectedNotice'), + disconnect: intl.get('screen.dashboard.cards.cavatica.disconnect'), + popover: { + title: intl.get('screen.dashboard.cards.cavatica.infoPopover.title'), + readMore: intl.get('screen.dashboard.cards.cavatica.infoPopover.readMore'), + content: intl.get('screen.dashboard.cards.cavatica.infoPopover.content'), + }, + firstProject: intl.get('screen.dashboard.cards.cavatica.createNewProject'), + newProject: intl.get('screen.dashboard.cards.cavatica.newProject'), + noProject: intl.get('screen.dashboard.cards.cavatica.noProjects'), + list: { + membersCount: (count: number) => + intl.get('screen.dashboard.cards.cavatica.membersCount', { + count, + }), + }, + connectCard: { + action: intl.get('global.connect'), + description: intl.get('screen.dashboard.cards.cavatica.disconnectedNotice'), + }, + createProjectModal: { + title: intl.get('screen.dashboard.cards.cavatica.newProject'), + requiredField: intl.get('global.forms.errors.requiredField'), + projectName: { + label: 'Project name', + placeholder: 'e.g. KF-NBL Neuroblastoma Aligned Reads', + }, + billingGroup: { + label: intl.get('screen.dashboard.cards.cavatica.billingGroups.label'), + empty: intl.get('screen.dashboard.cards.cavatica.billingGroups.empty'), + }, + okText: intl.get('screen.dashboard.cards.cavatica.createProject'), + cancelText: intl.get('screen.dashboard.cards.cavatica.cancelText'), + }, + error: { + billingGroups: { + title: intl.get('screen.dashboard.cards.error.title'), + subtitle: intl.get('api.cavatica.error.billingGroups.fetch'), + }, + fetch: { + title: intl.get('screen.dashboard.cards.error.title'), + subtitle: intl.get('screen.dashboard.cards.error.subtitle'), + }, + create: { + title: intl.get('screen.dashboard.cards.cavatica.error.create.title'), + subtitle: intl.get('screen.dashboard.cards.cavatica.error.create.subtitle'), + }, + email: SUPPORT_EMAIL, + contactSupport: intl.get('screen.dashboard.cards.error.contactSupport'), + }, + }} + /> ); }; diff --git a/src/views/Dashboard/components/DashboardCards/Notebook/index.tsx b/src/views/Dashboard/components/DashboardCards/Notebook/index.tsx index a48741f63..6e2407778 100644 --- a/src/views/Dashboard/components/DashboardCards/Notebook/index.tsx +++ b/src/views/Dashboard/components/DashboardCards/Notebook/index.tsx @@ -1,35 +1,73 @@ -import GridCard from '@ferlab/ui/core/view/v2/GridCard'; -import { Space, Typography, Button, Alert } from 'antd'; +import { useEffect, useState } from 'react'; import intl from 'react-intl-universal'; -import { DashboardCardProps } from 'views/Dashboard/components/DashboardCards'; -import CardHeader from 'views/Dashboard/components/CardHeader'; import { useDispatch } from 'react-redux'; +import { ApiOutlined, RocketOutlined } from '@ant-design/icons'; +import { IFenceService } from '@ferlab/ui/core/components/Widgets/AuthorizedStudies'; +import FencesAuthentificationModal from '@ferlab/ui/core/components/Widgets/AuthorizedStudies/FencesAuthentificationModal'; +import GridCard from '@ferlab/ui/core/view/v2/GridCard'; +import { Alert, Button, Space, Typography } from 'antd'; +import { isNotebookStatusInProgress, isNotebookStatusLaunched } from 'helpers/notebook'; +import CardHeader from 'views/Dashboard/components/CardHeader'; +import { DashboardCardProps } from 'views/Dashboard/components/DashboardCards'; +import { FENCE_NAMES } from 'common/fenceTypes'; import ZeppelinImg from 'components/assets/appache-zeppelin.png'; -import styles from './index.module.scss'; +import KidsFirstLoginIcon from 'components/Icons/KidsFirstLoginIcon'; +import NciIcon from 'components/Icons/NciIcon'; +import OpenInNewIcon from 'components/Icons/OpenInIcon'; +import useInterval from 'hooks/useInterval'; +import { TUserGroups } from 'services/api/user/models'; +import { useAtLeastOneFenceConnected, useFenceAuthentification } from 'store/fences'; +import { fenceDisconnection, fenceOpenAuhentificationTab } from 'store/fences/thunks'; import { useNotebook } from 'store/notebook'; -import { useEffect } from 'react'; -import { startNotebookCluster, getNotebookClusterStatus } from 'store/notebook/thunks'; +import { getNotebookClusterStatus, startNotebookCluster } from 'store/notebook/thunks'; import { useUser } from 'store/user'; -import { TUserGroups } from 'services/api/user/models'; -import useInterval from 'hooks/useInterval'; -import { isNotebookStatusInProgress, isNotebookStatusLaunched } from 'helpers/notebook'; -import { useFenceConnection } from 'store/fenceConnection'; -import { hasOneFenceConnected } from 'helpers/fence'; -import { fenceConnectionActions } from 'store/fenceConnection/slice'; -import { ApiOutlined, RocketOutlined } from '@ant-design/icons'; -import OpenInNewIcon from 'components/Icons/OpenInIcon'; + +import styles from './index.module.scss'; const { Text } = Typography; const REFRESH_INTERVAL = 30000; const Notebook = ({ id, key, className = '' }: DashboardCardProps) => { const dispatch = useDispatch(); + const gen3 = useFenceAuthentification(FENCE_NAMES.gen3); + const dcf = useFenceAuthentification(FENCE_NAMES.dcf); + const services: IFenceService[] = [ + { + fence: FENCE_NAMES.gen3, + name: 'Kids First Framework Services', + icon: , + onConnectToFence: () => { + dispatch(fenceOpenAuhentificationTab(FENCE_NAMES.gen3)); + }, + onDisconnectFromFence: () => { + dispatch(fenceDisconnection(FENCE_NAMES.gen3)); + }, + }, + { + fence: FENCE_NAMES.dcf, + name: 'NCI CRDC Framework Services', + icon: , + onConnectToFence: () => { + dispatch(fenceOpenAuhentificationTab(FENCE_NAMES.dcf)); + }, + onDisconnectFromFence: () => { + dispatch(fenceDisconnection(FENCE_NAMES.dcf)); + }, + }, + ]; + const [isFenceModalAuthentificationOpen, setIsFenceModalAuthentificationOpen] = + useState(false); const { url, isLoading, error, status } = useNotebook(); const { groups } = useUser(); - const { connectionStatus } = useFenceConnection(); + const onCloseFenceAuthentificationModal = () => { + setIsFenceModalAuthentificationOpen(false); + if (hasAtLeastOneAuthentificatedFence) { + handleStartNotebookCluster(); + } + }; - const isConnectedToFences = hasOneFenceConnected(connectionStatus); + const hasAtLeastOneAuthentificatedFence = useAtLeastOneFenceConnected(); const isAllowed = groups.includes(TUserGroups.INVESTIGATOR); const isProcessing = (isLoading || isNotebookStatusInProgress(status)) && !error; @@ -59,6 +97,13 @@ const Notebook = ({ id, key, className = '' }: DashboardCardProps) => { return ( <> + { Appache-Zeppelin-Logo {intl.getHTML('screen.dashboard.cards.notebook.description')} - {!isConnectedToFences && ( + {!hasAtLeastOneAuthentificatedFence && ( )} - {isConnectedToFences && !url && ( + {hasAtLeastOneAuthentificatedFence && !url && ( )} - {isConnectedToFences && url && isNotebookStatusLaunched(status) && ( + {hasAtLeastOneAuthentificatedFence && url && isNotebookStatusLaunched(status) && (