diff --git a/src/Discovery/Discovery.css b/src/Discovery/Discovery.css index 04b109b27c..d2db7c0d2e 100644 --- a/src/Discovery/Discovery.css +++ b/src/Discovery/Discovery.css @@ -412,6 +412,12 @@ .discovery-modal__attribute-value { text-align: left; padding: 4px; + overflow: hidden; + white-space: pre-wrap; /* CSS3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ } .discovery-modal__attribute-value--multiline { diff --git a/src/Discovery/Discovery.tsx b/src/Discovery/Discovery.tsx index cdb30e8ee9..2c79447b5f 100644 --- a/src/Discovery/Discovery.tsx +++ b/src/Discovery/Discovery.tsx @@ -195,6 +195,10 @@ const Discovery: React.FunctionComponent = (props: Props) => { const [permalinkCopied, setPermalinkCopied] = useState(false); const [exportingToWorkspace, setExportingToWorkspace] = useState(false); const [advSearchFilterHeight, setAdvSearchFilterHeight] = useState('100vh'); + const [downloadInProgress, setDownloadInProgress] = useState(false); + const [downloadStatusMessage, setDownloadStatusMessage] = useState({ + url: '', message: '', title: '', active: false, + }); const handleSearchChange = (ev) => { const { value } = ev.currentTarget; @@ -247,10 +251,11 @@ const Discovery: React.FunctionComponent = (props: Props) => { useEffect(() => { // If opening to a study by default, open that study - if (props.params.studyUID) { - const studyID = props.params.studyUID; + if (props.params.studyUID && props.studies.length > 0) { + const studyID = decodeURIComponent(props.params.studyUID); const defaultModalData = props.studies.find( (r) => r[config.minimalFieldMapping.uid] === studyID); + if (defaultModalData) { setPermalinkCopied(false); setModalData(defaultModalData); @@ -513,6 +518,10 @@ const Discovery: React.FunctionComponent = (props: Props) => { setExportingToWorkspace={setExportingToWorkspace} filtersVisible={filtersVisible} setFiltersVisible={setFiltersVisible} + downloadInProgress={downloadInProgress} + setDownloadInProgress={setDownloadInProgress} + downloadStatusMessage={downloadStatusMessage} + setDownloadStatusMessage={setDownloadStatusMessage} /> {/* Advanced search panel */} diff --git a/src/Discovery/DiscoveryActionBar.tsx b/src/Discovery/DiscoveryActionBar.tsx index 5712a13473..fbc92f2caf 100644 --- a/src/Discovery/DiscoveryActionBar.tsx +++ b/src/Discovery/DiscoveryActionBar.tsx @@ -3,6 +3,7 @@ import { Space, Popover, Button, + Modal, } from 'antd'; import { useHistory, useLocation } from 'react-router-dom'; import { @@ -10,11 +11,12 @@ import { RightOutlined, ExportOutlined, DownloadOutlined, + FileTextOutlined, } from '@ant-design/icons'; import FileSaver from 'file-saver'; import { DiscoveryConfig } from './DiscoveryConfig'; import { fetchWithCreds } from '../actions'; -import { manifestServiceApiPath, hostname } from '../localconf'; +import { manifestServiceApiPath, hostname, jobAPIPath } from '../localconf'; interface User { username: string @@ -27,8 +29,121 @@ interface Props { filtersVisible: boolean; setFiltersVisible: (boolean) => void; user: User, + downloadInProgress: boolean, + setDownloadInProgress: (boolean) => void, + downloadStatusMessage: { + title: string, + message: string, + active: boolean, + url: string + }, + setDownloadStatusMessage: (Object) => void; } +const handleDownloadZipClick = async ( + selectedResources: any[], + setDownloadInProgress: (boolean) => void, + setDownloadStatusMessage: (object) => void, +) => { + const studyIDs = selectedResources.map((study) => study.project_number); + setDownloadInProgress(true); + setDownloadStatusMessage({ + title: 'Your download is being prepared', + message: 'Please remain on this page while your download is being prepared.\n\n' + + 'When your download is ready, it will begin automatically. You can close this window.', + active: true, + url: '', + }); + + const handleJobError = () => { + setDownloadInProgress(false); + setDownloadStatusMessage({ + title: 'Download failed', + message: 'There was a problem preparing your download. ' + + 'Please consider using the Gen3 SDK for Python (w/ CLI) to download these files via a manifest.', + active: true, + url: '', + }); + }; + + fetchWithCreds({ + path: `${jobAPIPath}dispatch`, + method: 'POST', + body: JSON.stringify({ + action: 'batch-export', + input: { + study_ids: studyIDs, + }, + }), + }).then( + (dispatchResponse) => { + const { uid } = dispatchResponse.data; + if (dispatchResponse.status === 403 || dispatchResponse.status === 302) { + setDownloadInProgress(false); + setDownloadStatusMessage({ + title: 'Download failed', + message: 'Unable to authorize download. ' + + 'Please refresh the page and ensure you are logged in.', + active: true, + url: '', + }); + } else if (dispatchResponse.status !== 200 || !uid) { + handleJobError(); + } else { + const pollForJobStatusUpdate = () => { + fetchWithCreds({ path: `${jobAPIPath}status?UID=${uid}` }).then( + (statusResponse) => { + const { status } = statusResponse.data; + if (statusResponse.status !== 200 || !status) { + // usually empty status message means Sower can't find a job by its UID + handleJobError(); + } else if (status === 'Failed') { + fetchWithCreds({ path: `${jobAPIPath}output?UID=${uid}` }).then( + (outputResponse) => { + const { output } = outputResponse.data; + if (outputResponse.status !== 200 || !output) { + handleJobError(); + } else { + setDownloadStatusMessage({ + title: 'Download failed', + message: output, + active: true, + url: '', + }); + setDownloadInProgress(false); + } + }, + ).catch(handleJobError); + } else if (status === 'Completed') { + fetchWithCreds({ path: `${jobAPIPath}output?UID=${uid}` }).then( + (outputResponse) => { + const { output } = outputResponse.data; + if (outputResponse.status !== 200 || !output) { + handleJobError(); + } else { + setDownloadStatusMessage({ + title: 'Your download is ready', + message: 'Your download has been prepared. If your download doesn\'t start automatically, please follow this direct link: ', + active: true, + url: output, + }); + setDownloadInProgress(false); + setTimeout(() => window.open(output), 2000); + } + }, + ).catch(handleJobError); + } else { + setTimeout(pollForJobStatusUpdate, 5000); + } + }, + ); + }; + setTimeout(pollForJobStatusUpdate, 5000); + } + }, + ).catch(handleJobError); +}; + const handleDownloadManifestClick = (config: DiscoveryConfig, selectedResources: any[]) => { const { manifestFieldName } = config.features.exportToWorkspace; if (!manifestFieldName) { @@ -130,6 +245,41 @@ const DiscoveryActionBar = (props: Props) => { && ( {props.selectedResources.length} selected + { + props.config.features.exportToWorkspace.enableDownloadZip + && ( + + ) + } { props.config.features.exportToWorkspace.enableDownloadManifest && ( { : () => { handleRedirectToLoginClick(); }} type='text' disabled={props.selectedResources.length === 0} - icon={} + icon={} > {(props.user.username) ? `${props.config.features.exportToWorkspace.downloadManifestButtonText || 'Download Manifest'}` : `Login to ${props.config.features.exportToWorkspace.downloadManifestButtonText || 'Download Manifest'}`} + )} { {(props.user.username) ? 'Open In Workspace' : 'Login to Open In Workspace'} + props.setDownloadStatusMessage({ + title: '', message: '', active: false, url: '', + }) + } + > + Close + + )} + > + { props.downloadStatusMessage.message } + { + props.downloadStatusMessage.url + && {props.downloadStatusMessage.url} + } + )} diff --git a/src/Discovery/DiscoveryConfig.d.ts b/src/Discovery/DiscoveryConfig.d.ts index 41c9b3e29e..6cfb801ed5 100644 --- a/src/Discovery/DiscoveryConfig.d.ts +++ b/src/Discovery/DiscoveryConfig.d.ts @@ -6,6 +6,8 @@ export interface DiscoveryConfig { enableDownloadManifest: boolean downloadManifestButtonText?: string manifestFieldName: string + enableDownloadZip: boolean + downloadZipButtonText?: string } // explorationIntegration: { // enabled: boolean // not supported diff --git a/src/Discovery/DiscoveryDetails.tsx b/src/Discovery/DiscoveryDetails.tsx index 12c10b171f..35b30f1982 100644 --- a/src/Discovery/DiscoveryDetails.tsx +++ b/src/Discovery/DiscoveryDetails.tsx @@ -51,7 +51,7 @@ const DiscoveryDetails = (props: Props) => (