diff --git a/src/common/constants.js b/src/common/constants.js index 8beb74eb9..24017cacd 100644 --- a/src/common/constants.js +++ b/src/common/constants.js @@ -198,3 +198,5 @@ export const DB_GA_P = 'dbGaP'; export const generateUrlForDbGap = (dbGaPStudyId) => `https://www.ncbi.nlm.nih.gov/projects/gap/cgi-bin/study.cgi?study_id=${dbGaPStudyId}.v1.p1`; + +export const INTEGRATION_PREFIX = 'integration_'; diff --git a/src/components/ContextProvider/ContextProvider.js b/src/components/ContextProvider/ContextProvider.js index 9f5149ed5..6d6d73396 100644 --- a/src/components/ContextProvider/ContextProvider.js +++ b/src/components/ContextProvider/ContextProvider.js @@ -7,13 +7,12 @@ import { EGO_JWT_KEY } from 'common/constants'; import { ApiContext, initializeApi } from 'services/api'; import history, { HistoryContext } from 'services/history'; import { logoutAll } from 'services/login'; -import { provideFenceConnections, provideLoggedInUser } from 'stateProviders'; +import { provideLoggedInUser } from 'stateProviders'; import ScrollbarSizeProvider from './ScrollbarSizeProvider'; export default compose( provideLoggedInUser, - provideFenceConnections, injectState, )(({ children }) => ( diff --git a/src/components/Fence/FenceAuthorizedStudies.js b/src/components/Fence/FenceAuthorizedStudies.js index c1b28e7e0..e9ed989cc 100644 --- a/src/components/Fence/FenceAuthorizedStudies.js +++ b/src/components/Fence/FenceAuthorizedStudies.js @@ -1,18 +1,18 @@ -import React, { Fragment } from 'react'; -import { Spin } from 'antd'; -import { injectState } from 'freactal'; -import get from 'lodash/get'; +import React from 'react'; import { compose } from 'recompose'; +import useFenceConnections from 'hooks/useFenceConnections'; +import useFenceStudies from 'hooks/useFenceStudies'; import RightChevron from 'icons/DoubleChevronRightIcon'; import StackIcon from 'icons/StackIcon'; +import { withApi } from 'services/api'; import { withHistory } from 'services/history'; -import { fenceConnectionInitializeHoc } from 'stateProviders/provideFenceConnections'; import Column from 'uikit/Column'; import { Span } from 'uikit/Core'; import ExternalLink from 'uikit/ExternalLink'; import { PromptMessageContainer } from 'uikit/PromptMessage'; import Row from 'uikit/Row'; +import { Spinner } from 'uikit/Spinner'; import './FenceAuthorizedStudies.css'; @@ -29,47 +29,22 @@ const sqonForStudy = (studyId) => ({ ], }); -const FenceProjectList = ({ history, fenceConnectionsInitialized, authStudies }) => - !fenceConnectionsInitialized ? ( - - ) : ( - authStudies.map(({ id, studyShortName }) => { - const sqon = sqonForStudy(id); - return ( - - - - - - - {studyShortName} ({`${id}`}) - - - - - history.push(`/search/file?sqon=${encodeURI(JSON.stringify(sqon))}`)} - > - {'View data files'} - - - - - ); - }) - ); - const FenceAuthorizedStudies = compose( + withApi, withHistory, - injectState, - fenceConnectionInitializeHoc, -)(({ fence, history, state: { fenceStudies, fenceConnectionsInitialized } }) => { - const authStudies = get(fenceStudies, `${fence}.authorizedStudies`, []); +)(({ onCloseModalCb, api, fence, history }) => { + const { isFetchingAllFenceConnections } = useFenceConnections(api, [fence]); + const { isFetchingAllFenceStudies, fenceAuthStudies } = useFenceStudies(api); + + const isLoadingStudies = isFetchingAllFenceConnections || isFetchingAllFenceStudies; + if (isLoadingStudies) { + return ; + } return (
- {authStudies.length ? ( - + {fenceAuthStudies.length ? ( + <> {' '} @@ -77,13 +52,34 @@ const FenceAuthorizedStudies = compose( - + {fenceAuthStudies.map(({ id, studyShortName }) => ( + + + + + + + {studyShortName} ({`${id}`}) + + + + + { + history.push( + `/search/file?sqon=${encodeURI(JSON.stringify(sqonForStudy(id)))}`, + ); + onCloseModalCb(); + }} + > + {'View data files'} + + + + + ))} - + ) : ( diff --git a/src/components/FileRepo/CustomColumns/ActionsColumn.js b/src/components/FileRepo/CustomColumns/ActionsColumn.js index c084f9de5..2b14f5eb1 100644 --- a/src/components/FileRepo/CustomColumns/ActionsColumn.js +++ b/src/components/FileRepo/CustomColumns/ActionsColumn.js @@ -20,7 +20,6 @@ import Tooltip from 'uikit/Tooltip'; import { ControlledIcon } from '../ui'; import './customColumns.css'; -const enhance = compose(withApi); const FenceDownloadButton = ({ fence, kfId }) => // DCF files currently aren't available to download, so we show tooltip and grey out button @@ -59,29 +58,6 @@ FenceDownloadButton.propTypes = { kfId: PropTypes.string.isRequired, }; -const ActionItems = ({ value, fence, hasAccess }) => ( - <> - {hasAccess ? ( - - ) : ( - You do not have access to this file.} - > - - - )} - -); - -ActionItems.propTypes = { - fence: PropTypes.string.isRequired, - file: PropTypes.object.isRequired, - hasAccess: PropTypes.bool.isRequired, - value: PropTypes.any.isRequired, -}; - const ActionsColumn = ({ value, api, fenceAcls }) => ( ( }, }} render={({ loading: loadingQuery, data }) => { + if (loadingQuery) { + return ( + + + + ); + } const file = get(data, 'file.hits.edges[0].node', {}); const acl = file.acl || []; const repository = file.repository; @@ -125,7 +108,19 @@ const ActionsColumn = ({ value, api, fenceAcls }) => ( {loadingQuery ? ( ) : ( - + <> + {hasAccess ? ( + + ) : ( + You do not have access to this file.} + > + + + )} + )} ); @@ -139,4 +134,4 @@ ActionsColumn.propTypes = { fenceAcls: PropTypes.arrayOf(PropTypes.string).isRequired, }; -export default enhance(ActionsColumn); +export default compose(withApi)(ActionsColumn); diff --git a/src/components/FileRepo/FileRepo.js b/src/components/FileRepo/FileRepo.js index 1b408c2d8..9ddb4158a 100644 --- a/src/components/FileRepo/FileRepo.js +++ b/src/components/FileRepo/FileRepo.js @@ -10,6 +10,7 @@ import isObject from 'lodash/isObject'; import PropTypes from 'prop-types'; import { compose } from 'recompose'; +import { FENCES } from 'common/constants'; import { arrangerProjectId } from 'common/injectGlobals'; import translateSQON from 'common/translateSQONValue'; import ArrangerConnectionGuard from 'components/ArrangerConnectionGuard'; @@ -18,10 +19,10 @@ import SaveQuery from 'components/LoadShareSaveDeleteQuery/SaveQuery'; import ShareQuery from 'components/LoadShareSaveDeleteQuery/ShareQuery'; import SQONURL from 'components/SQONURL'; import { FileRepoStatsQuery } from 'components/Stats'; +import useFenceConnections from 'hooks/useFenceConnections'; import DownloadIcon from 'icons/DownloadIcon'; import { TRACKING_EVENTS, trackUserInteraction } from 'services/analyticsTracking'; import { withApi } from 'services/api'; -import { fenceConnectionInitializeHoc } from 'stateProviders/provideFenceConnections'; import { closeModal, openModal } from 'store/actions/modal'; import { selectModalId } from 'store/selectors/modal'; import theme from 'theme/defaultTheme'; @@ -29,6 +30,7 @@ import { fillCenter } from 'theme/tempTheme.module.css'; import Column from 'uikit/Column'; import { FilterInput } from 'uikit/Input'; import Row from 'uikit/Row'; +import { Spinner } from 'uikit/Spinner'; import Tooltip from 'uikit/Tooltip'; import CavaticaConnectModal from '../cavatica/CavaticaConnectModal'; @@ -64,7 +66,7 @@ const mapDispatchToProps = (dispatch) => ({ const connector = connect(mapStateToProps, mapDispatchToProps); -const enhance = compose(connector, injectState, withApi, fenceConnectionInitializeHoc); +const enhance = compose(connector, injectState, withApi); const FileRepo = ({ state, @@ -79,229 +81,245 @@ const FileRepo = ({ closeModal, openModal, ...props -}) => ( - ( - - connecting || connectionError ? ( -
- {connectionError ? ( - `Unable to connect to the file repo, please try again later` - ) : ( - - )} -
- ) : ( - { - const selectionSQON = props.selectedTableRows.length - ? replaceSQON({ - op: 'and', - content: [ - { - op: 'in', - content: { field: 'kf_id', value: props.selectedTableRows }, - }, - ], - }) - : url.sqon; +}) => { + const { + //needed in order to avoid rendering the main component on mount before fetching fences. + isCheckingIfFenceConnectionsFetchIsNeeded, + isFetchingAllFenceConnections, + fenceConnections, + fencesAllAcls, + } = useFenceConnections(props.api, FENCES); - const showConnectModal = openModalId === CAVATICA_CONNECT_FILE_REPO_MODAL_ID; - const showCavaticaCopyModal = openModalId === CAVATICA_COPY_FILE_REPO_MODAL_ID; + if (isCheckingIfFenceConnectionsFetchIsNeeded || isFetchingAllFenceConnections) { + return ; + } + return ( + ( + + connecting || connectionError ? ( +
+ {connectionError ? ( + `Unable to connect to the file repo, please try again later` + ) : ( + + )} +
+ ) : ( + { + const selectionSQON = props.selectedTableRows.length + ? replaceSQON({ + op: 'and', + content: [ + { + op: 'in', + content: { field: 'kf_id', value: props.selectedTableRows }, + }, + ], + }) + : url.sqon; - const closeCopyCavaticaModal = () => closeModal(CAVATICA_COPY_FILE_REPO_MODAL_ID); + const showConnectModal = openModalId === CAVATICA_CONNECT_FILE_REPO_MODAL_ID; + const showCavaticaCopyModal = openModalId === CAVATICA_COPY_FILE_REPO_MODAL_ID; - return ( - <> - {showConnectModal && ( - { - closeModal(CAVATICA_CONNECT_FILE_REPO_MODAL_ID); - openModal(CAVATICA_COPY_FILE_REPO_MODAL_ID); - }} - onCancelCB={() => closeModal(CAVATICA_CONNECT_FILE_REPO_MODAL_ID)} - /> - )} - {showCavaticaCopyModal && ( - - )} - - - - - { - trackFileRepoInteraction({ - category: TRACKING_EVENTS.categories.fileRepo.dataTable, - action: TRACKING_EVENTS.actions.query.clear, - }); - trackFileRepoInteraction({ - category: 'File Repo', - action: TRACKING_EVENTS.actions.query.abandoned, - label: 'cleared SQON', - value: 1, - }); - }} - /> - {url.sqon && Object.keys(url.sqon).length > 0 && ( - closeModal(CAVATICA_COPY_FILE_REPO_MODAL_ID); + + return ( + <> + {showConnectModal && ( + { + closeModal(CAVATICA_CONNECT_FILE_REPO_MODAL_ID); + openModal(CAVATICA_COPY_FILE_REPO_MODAL_ID); + }} + onCancelCB={() => closeModal(CAVATICA_CONNECT_FILE_REPO_MODAL_ID)} + /> + )} + {showCavaticaCopyModal && ( + + )} + + + + + ( - - - - - - - - - )} + {...{ translateSQONValue }} + onClear={() => { + trackFileRepoInteraction({ + category: TRACKING_EVENTS.categories.fileRepo.dataTable, + action: TRACKING_EVENTS.actions.query.clear, + }); + trackFileRepoInteraction({ + category: 'File Repo', + action: TRACKING_EVENTS.actions.query.abandoned, + label: 'cleared SQON', + value: 1, + }); + }} /> - )} - - - - - - {'Cavatica is a cloud processing platform where files can be ' + - 'linked (not duplicated) and used immediately.'} - - } - > -
+ + {'Cavatica is a cloud processing platform where files can be ' + + 'linked (not duplicated) and used immediately.'} + + } > - {'Analyze in Cavatica'} - - - <> - - - - - } - customTypes={{ - // eslint-disable-next-line react/display-name,react/prop-types - access: ({ value }) => ( - - {typeof value !== 'boolean' ? ( - `` - ) : value ? ( - - ) : ( - - )} + + + <> + + + - ), - }} - showFilterInput={false} - InputComponent={(props) => ( - - )} - customColumns={customTableColumns({ - theme, - userProjectIds, - fenceAcls: state.fenceAcls, - })} - filterInputPlaceholder={'Filter table'} - columnDropdownText="Columns" - fieldTypesForFilter={['text', 'keyword', 'id']} - maxPagesOptions={5} - onFilterChange={(val) => { - if (val !== '') { + } + customTypes={{ + // eslint-disable-next-line react/display-name,react/prop-types + access: ({ value }) => ( + + {typeof value !== 'boolean' ? ( + `` + ) : value ? ( + + ) : ( + + )} + + ), + }} + showFilterInput={false} + InputComponent={(props) => ( + + )} + customColumns={customTableColumns({ + theme, + userProjectIds, + fenceAcls: fencesAllAcls, + })} + filterInputPlaceholder={'Filter table'} + columnDropdownText="Columns" + fieldTypesForFilter={['text', 'keyword', 'id']} + maxPagesOptions={5} + onFilterChange={(val) => { + if (val !== '') { + trackFileRepoInteraction({ + category: TRACKING_EVENTS.categories.fileRepo.dataTable, + action: TRACKING_EVENTS.actions.filter, + label: val, + }); + } + if (props.onFilterChange) { + props.onFilterChange(val); + } + }} + onTableExport={({ files }) => { trackFileRepoInteraction({ category: TRACKING_EVENTS.categories.fileRepo.dataTable, - action: TRACKING_EVENTS.actions.filter, - label: val, + action: 'Export TSV', + label: files, }); + }} + exportTSVText={ + <> + + {'Export TSV'} + } - if (props.onFilterChange) { - props.onFilterChange(val); - } - }} - onTableExport={({ files }) => { - trackFileRepoInteraction({ - category: TRACKING_EVENTS.categories.fileRepo.dataTable, - action: 'Export TSV', - label: files, - }); - }} - exportTSVText={ - <> - - {'Export TSV'} - - } - /> + /> + - - - - ); - }} - /> - ) - } - /> - )} - /> -); + + + ); + }} + /> + ) + } + /> + )} + /> + ); +}; FileRepo.propTypes = { state: PropTypes.object.isRequired, diff --git a/src/components/Login/utils.js b/src/components/Login/utils.js index 9fd689b6d..2f0eb617a 100644 --- a/src/components/Login/utils.js +++ b/src/components/Login/utils.js @@ -4,6 +4,7 @@ import isArrayLikeObject from 'lodash/isArrayLikeObject'; import toLower from 'lodash/toLower'; import { CAVATICA, FENCES } from 'common/constants'; +import { INTEGRATION_PREFIX } from 'common/constants'; import { getUser as getCavaticaUser } from 'services/cavatica'; import { getAccessToken } from 'services/fence'; import { createProfile, getProfile } from 'services/profiles'; @@ -105,3 +106,6 @@ export const fetchIntegrationTokens = ({ setIntegrationToken, api }) => { }); }); }; + +export const hasIntegrationTokenForFence = (fenceName) => + !!localStorage.getItem(`${INTEGRATION_PREFIX}${fenceName}`); diff --git a/src/components/UserDashboard/AuthorizedStudies/StudiesConnected.js b/src/components/UserDashboard/AuthorizedStudies/StudiesConnected.js index 796071d48..4720bbff8 100644 --- a/src/components/UserDashboard/AuthorizedStudies/StudiesConnected.js +++ b/src/components/UserDashboard/AuthorizedStudies/StudiesConnected.js @@ -1,131 +1,113 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { compose } from 'recompose'; -import { injectState } from 'freactal'; import { kfWebRoot } from 'common/injectGlobals'; +import { TRACKING_EVENTS, trackUserInteraction } from 'services/analyticsTracking'; +import { createAcceptedFilesByUserStudySqon, createStudyIdSqon } from 'services/fileAccessControl'; +import { withHistory } from 'services/history'; import Column from 'uikit/Column'; import { Box, Link } from 'uikit/Core'; -import { withApi } from 'services/api'; -import { withHistory } from 'services/history'; - -import { PromptMessageContainer, PromptMessageHeading, PromptMessageContent } from '../styles'; import Info from '../Info'; -import { createStudyIdSqon, createAcceptedFilesByUserStudySqon } from 'services/fileAccessControl'; -import Study from './Study'; -import { trackUserInteraction, TRACKING_EVENTS } from 'services/analyticsTracking'; -import { studiesList } from '../UserDashboard.module.css'; -import PropTypes from 'prop-types'; - -const NoAuthorizedStudiesMessage = ({ user }) => ( - - - {" You are connected to a data repository partner, but you don't have access to controlled data\n" + - ' yet.'} - - - Start applying from our{' '} - - studies and access page. - - - -); - -NoAuthorizedStudiesMessage.propTypes = { - user: PropTypes.object.isRequired, -}; +import { PromptMessageContainer, PromptMessageContent, PromptMessageHeading } from '../styles'; -const renderNoAuthorizedStudies = ({ loggedInUser }) => ( - - - - - {' '} - -); +import Study from './Study'; -renderNoAuthorizedStudies.propTypes = { - loggedInUser: PropTypes.object.isRequired, -}; +import { studiesList } from '../UserDashboard.module.css'; -const renderAuthorizedStudies = ({ fenceAuthStudies, history }) => { - const studiesById = fenceAuthStudies.reduce((obj, study) => { +const groupStudiesById = (fenceAuthStudies) => + fenceAuthStudies.reduce((obj, study) => { obj[study.id] = study; return obj; }, {}); - const onStudyTotalClick = (studyId) => () => { - trackUserInteraction({ - category: TRACKING_EVENTS.categories.user.dashboard.widgets.authorizedStudies, - action: `Studies Total: ${TRACKING_EVENTS.actions.click}`, - label: `studyId: ${studyId}`, - }); - history.push(`/search/file?sqon=${encodeURI(JSON.stringify(createStudyIdSqon(studyId)))}`); - }; - - const onStudyAuthorizedClick = (studyId, eventOrigin) => { - trackUserInteraction({ - category: TRACKING_EVENTS.categories.user.dashboard.widgets.authorizedStudies, - action: `${eventOrigin}: ${TRACKING_EVENTS.actions.click}`, - label: `studyId: ${studyId}`, - }); - const consentCodes = studiesById[studyId].acl; - history.push( - `/search/file?sqon=${encodeURI( - JSON.stringify(createAcceptedFilesByUserStudySqon(consentCodes)({ studyId })), - )}`, - ); - }; - return fenceAuthStudies.map((study) => ( - - )); -}; +const StudiesConnected = compose(withHistory)(({ loggedInUser, fenceAuthStudies, history }) => { + const hasAuthorizedStudies = fenceAuthStudies && fenceAuthStudies.length > 0; + if (hasAuthorizedStudies) { + const studiesById = groupStudiesById(fenceAuthStudies); -renderAuthorizedStudies.propTypes = { - fenceAuthStudies: PropTypes.array.isRequired, - history: PropTypes.array.isRequired, -}; + const onStudyTotalClick = (studyId) => () => { + trackUserInteraction({ + category: TRACKING_EVENTS.categories.user.dashboard.widgets.authorizedStudies, + action: `Studies Total: ${TRACKING_EVENTS.actions.click}`, + label: `studyId: ${studyId}`, + }); + history.push(`/search/file?sqon=${encodeURI(JSON.stringify(createStudyIdSqon(studyId)))}`); + }; -const enhance = compose(injectState, withHistory, withApi); - -const StudiesConnected = enhance( - ({ state: { loggedInUser, fenceConnections, fenceAuthStudies }, history }) => { - const hasAuthorizedStudies = fenceAuthStudies && fenceAuthStudies.length > 0; - if (hasAuthorizedStudies) { - return ( - - {renderAuthorizedStudies({ - fenceAuthStudies, - fenceConnections, - history, - })} - + const onStudyAuthorizedClick = (studyId, eventOrigin) => { + trackUserInteraction({ + category: TRACKING_EVENTS.categories.user.dashboard.widgets.authorizedStudies, + action: `${eventOrigin}: ${TRACKING_EVENTS.actions.click}`, + label: `studyId: ${studyId}`, + }); + const consentCodes = studiesById[studyId].acl; + history.push( + `/search/file?sqon=${encodeURI( + JSON.stringify(createAcceptedFilesByUserStudySqon(consentCodes)({ studyId })), + )}`, ); - } + }; + return ( + + {fenceAuthStudies.map((study) => ( + + ))} + + ); + } - return {renderNoAuthorizedStudies({ loggedInUser })}; - }, -); + return ( + + + + + + {'You are connected to a data repository partner,' + + " but you don't have access to controlled data\n" + + ' yet.'} + + + Start applying from our{' '} + + studies and access page. + + + + + {' '} + + + ); +}); + +StudiesConnected.propTypes = { + loggedInUser: PropTypes.object, + fenceAuthStudies: PropTypes.array, + history: PropTypes.shape({ + push: PropTypes.func, + }), +}; export default StudiesConnected; diff --git a/src/components/UserDashboard/AuthorizedStudies/index.js b/src/components/UserDashboard/AuthorizedStudies/index.js index b448e180b..5e932d49a 100644 --- a/src/components/UserDashboard/AuthorizedStudies/index.js +++ b/src/components/UserDashboard/AuthorizedStudies/index.js @@ -1,66 +1,69 @@ -import React, { Fragment } from 'react'; -import { compose } from 'recompose'; +import React from 'react'; +import Card from '@ferlab/ui/core/view/GridCard'; +import { Badge, Button } from 'antd'; import { injectState } from 'freactal'; import isEmpty from 'lodash/isEmpty'; +import { compose } from 'recompose'; + +import { FENCES } from 'common/constants'; +import useFenceConnections from 'hooks/useFenceConnections'; +import useFenceStudies from 'hooks/useFenceStudies'; import DownloadController from 'icons/DownloadController'; -import StudiesConnected from './StudiesConnected'; -import { fenceConnectionInitializeHoc } from 'stateProviders/provideFenceConnections'; + import AccessGate from '../../AccessGate'; import Info from '../Info'; -import { Badge, Button } from 'antd'; -import Card from '@ferlab/ui/core/view/GridCard'; + +import StudiesConnected from './StudiesConnected'; + import { antCardHeader } from '../../CohortBuilder/Summary/Cards/StudiesChart.module.css'; -const AuthorizedStudies = compose( - injectState, - fenceConnectionInitializeHoc, -)( - ({ - state: { loggedInUser, fenceConnectionsInitialized, fenceConnections, fenceAuthStudies }, - }) => { - const inactive = !fenceConnectionsInitialized; - return ( - - Authorized Studies  +const AuthorizedStudies = compose(injectState)(({ loggedInUser, api }) => { + const { isFetchingAllFenceConnections, fenceConnections } = useFenceConnections(api, FENCES); + const { isFetchingAllFenceStudies, fenceAuthStudies } = useFenceStudies(api); + + const isLoadingData = isFetchingAllFenceConnections || isFetchingAllFenceStudies; + const hasNoFenceConnections = isEmpty(fenceConnections); + + return ( + + Authorized Studies  - - - } - loading={inactive} - > - {isEmpty(fenceConnections) ? ( - - - To access controlled study files,{' '} - connect to our data repository partners. - - } - > - - - - - ) : ( - - )} - - ); - }, -); + + + } + loading={isLoadingData} + > + {hasNoFenceConnections ? ( + <> + + To access controlled study files,{' '} + connect to our data repository partners. + + } + > + + + + + ) : ( + + )} + + ); +}); export default AuthorizedStudies; diff --git a/src/components/UserDashboard/index.js b/src/components/UserDashboard/index.js index c7589a993..774981e40 100644 --- a/src/components/UserDashboard/index.js +++ b/src/components/UserDashboard/index.js @@ -1,24 +1,24 @@ -import * as React from 'react'; -import { branch, compose, renderComponent } from 'recompose'; +import React from 'react'; +import Grid from '@ferlab/ui/core/layout/Grid'; +import Card from '@ferlab/ui/core/view/GridCard'; import { injectState } from 'freactal'; +import { compose } from 'recompose'; -import { withApi } from 'services/api'; -import SavedQueries from './SavedQueries'; -import AuthorizedStudies from './AuthorizedStudies'; -import CavaticaProjects from './CavaticaProjects'; -import ParticipantSets from './ParticipantSets'; - -import Card from '@ferlab/ui/core/view/GridCard'; -import Grid from '@ferlab/ui/core/layout/Grid'; import { MemberResearchInterestsChart, MostFrequentDiagnosesChart, MostParticipantsStudiesChart, } from 'components/Charts'; +import { withApi } from 'services/api'; +import { Spinner } from 'uikit/Spinner'; + +import AuthorizedStudies from './AuthorizedStudies'; +import CavaticaProjects from './CavaticaProjects'; +import ParticipantSets from './ParticipantSets'; +import SavedQueries from './SavedQueries'; import './UserDashboard.module.css'; import './UserDashboard.scss'; - import { dashboardTitle, userDashboardContainer, @@ -28,32 +28,38 @@ import { export default compose( injectState, withApi, - branch( - ({ state: { loggedInUser } }) => !loggedInUser, - renderComponent(() =>
), - ), -)(({ state: { loggedInUser }, api }) => ( -
-
-

My Dashboard

- - - - - - Member Research Interests}> - - - Most Frequent Diagnoses}> - - - My Participant Sets} - > - - - +)(({ state: { loggedInUser }, api }) => { + if (!loggedInUser) { + return ( +
+
+ +
+ ); + } + return ( +
+
+

My Dashboard

+ + + + + + Member Research Interests}> + + + Most Frequent Diagnoses}> + + + My Participant Sets} + > + + + +
-
-)); + ); +}); diff --git a/src/components/UserProfile/Integration.js b/src/components/UserProfile/Integration.js index b537eb64a..ab697b796 100644 --- a/src/components/UserProfile/Integration.js +++ b/src/components/UserProfile/Integration.js @@ -1,26 +1,33 @@ /* eslint-disable react/prop-types */ import React from 'react'; -import { compose, setPropTypes } from 'recompose'; +import { connect as reduxConnect } from 'react-redux'; +import { BookOutlined } from '@ant-design/icons'; +import { notification } from 'antd'; import { injectState } from 'freactal'; -import { withApi } from 'services/api'; import get from 'lodash/get'; -import { fenceConnectionInitializeHoc } from 'stateProviders/provideFenceConnections'; +import PropTypes from 'prop-types'; +import { compose, setPropTypes } from 'recompose'; + +import useFenceConnections from 'hooks/useFenceConnections'; +import { + setUserDimension, + TRACKING_EVENTS, + trackUserInteraction, +} from 'services/analyticsTracking'; +import { withApi } from 'services/api'; import { convertTokenToUser, deleteFenceTokens, fenceConnect, getAccessToken, } from 'services/fence'; -import { - setUserDimension, - TRACKING_EVENTS, - trackUserInteraction, -} from 'services/analyticsTracking'; -import PropTypes from 'prop-types'; -import IntegrationManager from './IntegrationManager'; -import { BookOutlined } from '@ant-design/icons'; -import { notification } from 'antd'; +import { addFenceConnection } from 'store/actionCreators/fenceConnections'; +import { removeFenceConnection } from 'store/actionCreators/fenceConnections'; +import { computeAclsForConnection } from 'store/actionCreators/fenceConnections'; +import { fetchFenceStudiesIfNeeded } from 'store/actionCreators/fenceStudies'; +import { removeFenceStudies } from 'store/actionCreators/fenceStudies'; +import IntegrationManager from './IntegrationManager'; const AUTHORIZED_STUDIES_DIM = '5'; const CAVATICA_DIM = '6'; const DCF_DIM = '7'; @@ -52,12 +59,21 @@ const trackFenceAction = async ({ fence, fenceDetails, category, action, label } await trackUserInteraction({ category, action, label }); }; -const disconnect = async ({ fence, api, setConnecting, effects, setError }) => { +const disconnect = async ({ + fence, + api, + setConnecting, + effects, + setError, + removeFenceConnection, + removeFenceStudies, +}) => { setConnecting(true); try { await deleteFenceTokens(api, fence); await effects.setIntegrationToken(fence, null); - await effects.removeFenceConnection(fence); + removeFenceConnection(fence); + removeFenceStudies(fence); await trackFenceAction({ fence, fenceDetails: '', @@ -71,15 +87,23 @@ const disconnect = async ({ fence, api, setConnecting, effects, setError }) => { setConnecting(false); }; -const connect = async ({ fence, api, setConnecting, effects, setError }) => { +const connect = async ({ + fence, + api, + setConnecting, + effects, + setError, + fetchFenceStudiesIfNeeded, + addFenceConnection, +}) => { setConnecting(true); try { await fenceConnect(api, fence); const token = await getAccessToken(api, fence); effects.setIntegrationToken(fence, token); const details = convertTokenToUser(token); - effects.addFenceConnection({ fence, details }); - await effects.fetchFenceStudies({ api, fence, details }); + addFenceConnection(fence, details); + fetchFenceStudiesIfNeeded(api, fence, computeAclsForConnection(details)); setConnecting(false); notification.success({ message: 'Success', @@ -106,21 +130,31 @@ const connect = async ({ fence, api, setConnecting, effects, setError }) => { function Integration(props) { const { + addFenceConnection, + fetchFenceStudiesIfNeeded, + removeFenceConnection, + removeFenceStudies, fence, - state: { fenceConnectionsInitialized, fenceConnections }, logo, description, effects, api, } = props; + const { + isCheckingIfFenceConnectionsFetchIsNeeded, + isFetchingAllFenceConnections, + fenceConnections, + } = useFenceConnections(api, [fence]); + const isLoadingFence = isCheckingIfFenceConnectionsFetchIsNeeded || isFetchingAllFenceConnections; + return ( , @@ -132,6 +166,8 @@ function Integration(props) { fence, api, effects, + addFenceConnection, + fetchFenceStudiesIfNeeded, }, }} disConnection={{ @@ -140,15 +176,28 @@ function Integration(props) { fence, api, effects, + removeFenceConnection, + removeFenceStudies, }, }} /> ); } +const mapDispatchToProps = (dispatch) => ({ + addFenceConnection: (fenceName, connection) => + dispatch(addFenceConnection(fenceName, connection)), + fetchFenceStudiesIfNeeded: (api, fenName, acls) => + dispatch(fetchFenceStudiesIfNeeded(api, fenName, acls)), + removeFenceConnection: (fenceName) => dispatch(removeFenceConnection(fenceName)), + removeFenceStudies: (fenceName) => dispatch(removeFenceStudies(fenceName)), +}); + +const connector = reduxConnect(null, mapDispatchToProps); + const Enhanced = compose( + connector, injectState, - fenceConnectionInitializeHoc, withApi, setPropTypes({ logo: PropTypes.node.isRequired, @@ -158,6 +207,10 @@ const Enhanced = compose( state: PropTypes.object.isRequired, effects: PropTypes.object.isRequired, api: PropTypes.func.isRequired, + addFenceConnection: PropTypes.func, + fetchFenceStudiesIfNeeded: PropTypes.func, + removeFenceConnection: PropTypes.func, + removeFenceStudies: PropTypes.func, }), )(Integration); diff --git a/src/components/UserProfile/IntegrationItem.js b/src/components/UserProfile/IntegrationItem.js index 4cfa033fd..1b20edc76 100644 --- a/src/components/UserProfile/IntegrationItem.js +++ b/src/components/UserProfile/IntegrationItem.js @@ -93,7 +93,7 @@ const IntegrationItem = (props) => { , ]} > - + )}
diff --git a/src/components/cavatica/CavaticaCopyMultipleFilesModal.js b/src/components/cavatica/CavaticaCopyMultipleFilesModal.js index 575921f2e..b4364877e 100644 --- a/src/components/cavatica/CavaticaCopyMultipleFilesModal.js +++ b/src/components/cavatica/CavaticaCopyMultipleFilesModal.js @@ -1,15 +1,11 @@ -import * as React from 'react'; -import { withRouter } from 'react-router-dom'; +import React from 'react'; import { Link } from 'react-router-dom'; -import { Alert, Button, Modal, notification, Typography } from 'antd'; -import { injectState } from 'freactal'; +import { Alert, Button, Modal, notification, Spin, Typography } from 'antd'; import flatten from 'lodash/flatten'; import PropTypes from 'prop-types'; -import { compose } from 'recompose'; import { FENCES } from 'common/constants'; import { TRACKING_EVENTS, trackUserInteraction } from 'services/analyticsTracking'; -import { withApi } from 'services/api'; import { graphql } from 'services/arranger'; import { getUserStudyPermission } from 'services/fileAccessControl'; @@ -34,8 +30,6 @@ const shapeStudyAggs = (studyAggs = []) => })) .sort(({ count }, { count: nextCount }) => nextCount - count); -const enhance = compose(injectState, withRouter, withApi); - const getSqonOrDefault = ( sqon, defaultVal = { @@ -54,24 +48,22 @@ class CavaticaCopyMultipleFilesModal extends React.Component { fileStudyData: {}, fileAuthInitialized: false, authorizedFilesCombined: [], + isLoadingSelectedFilesSummary: false, }; static propTypes = { api: PropTypes.func.isRequired, sqon: PropTypes.object, fileIds: PropTypes.arrayOf(PropTypes.string), - state: PropTypes.object, onComplete: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, + fenceConnections: PropTypes.object, + loggedInUser: PropTypes.object, }; async componentDidMount() { - const { - fileIds, - api, - state: { fenceConnections }, - sqon, - } = this.props; + const { fileIds, api, sqon, fenceConnections } = this.props; + this.setState({ isLoadingSelectedFilesSummary: true }); let ids = fileIds; if (!ids || ids.length === 0) { ids = await graphql(api)({ @@ -148,11 +140,12 @@ class CavaticaCopyMultipleFilesModal extends React.Component { fileAuthInitialized: true, authorizedFilesCombined: flatten(Object.values({ ...authFiles })), filesSelected: ids, + isLoadingSelectedFilesSummary: false, }); } render() { - const { state, onComplete, onCancel } = this.props; + const { onComplete, onCancel, loggedInUser, fenceConnections } = this.props; const { addingProject, @@ -163,9 +156,10 @@ class CavaticaCopyMultipleFilesModal extends React.Component { filesSelected, fileAuthInitialized, fileStudyData, + isLoadingSelectedFilesSummary, } = this.state; - const hasFenceConnection = Object.keys(state.fenceConnections).length > 0; + const hasFenceConnection = Object.keys(fenceConnections).length > 0; const isFilesSelected = filesSelected && filesSelected.length > 0; const unauthFilesWarning = unauthorizedFiles && unauthorizedFiles.length > 0; return ( @@ -206,13 +200,13 @@ class CavaticaCopyMultipleFilesModal extends React.Component { }); } - trackUserInteraction({ + await trackUserInteraction({ category: TRACKING_EVENTS.categories.fileRepo.actionsSidebar, action: 'Copied Files to Cavatica Project', label: JSON.stringify({ files: uuids.length, uuids }), }); } catch (e) { - trackUserInteraction({ + await trackUserInteraction({ category: TRACKING_EVENTS.categories.fileRepo.actionsSidebar, action: 'Copied Files to Cavatica Project FAILED', label: e.message ? e.message : null, @@ -230,9 +224,12 @@ class CavaticaCopyMultipleFilesModal extends React.Component { key={'submit'} render={({ onClick, loading }) => (