Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UI components and polling for zip downloads #906

Merged
merged 19 commits into from
Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Discovery/Discovery.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
13 changes: 11 additions & 2 deletions src/Discovery/Discovery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ const Discovery: React.FunctionComponent<Props> = (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;
Expand Down Expand Up @@ -247,10 +251,11 @@ const Discovery: React.FunctionComponent<Props> = (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);
Expand Down Expand Up @@ -513,6 +518,10 @@ const Discovery: React.FunctionComponent<Props> = (props: Props) => {
setExportingToWorkspace={setExportingToWorkspace}
filtersVisible={filtersVisible}
setFiltersVisible={setFiltersVisible}
downloadInProgress={downloadInProgress}
setDownloadInProgress={setDownloadInProgress}
downloadStatusMessage={downloadStatusMessage}
setDownloadStatusMessage={setDownloadStatusMessage}
/>

{/* Advanced search panel */}
Expand Down
177 changes: 175 additions & 2 deletions src/Discovery/DiscoveryActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ import {
Space,
Popover,
Button,
Modal,
} from 'antd';
import { useHistory, useLocation } from 'react-router-dom';
import {
LeftOutlined,
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
Expand All @@ -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) {
Expand Down Expand Up @@ -130,6 +245,41 @@ const DiscoveryActionBar = (props: Props) => {
&& (
<Space>
<span className='discovery-export__selected-ct'>{props.selectedResources.length} selected</span>
{
props.config.features.exportToWorkspace.enableDownloadZip
&& (
<Button
onClick={() => {
if (props.user.username) {
handleDownloadZipClick(
props.selectedResources,
props.setDownloadInProgress,
props.setDownloadStatusMessage,
);
} else {
handleRedirectToLoginClick();
}
}}
type='text'
disabled={props.selectedResources.length === 0 || props.downloadInProgress === true}
icon={<DownloadOutlined />}
loading={props.downloadInProgress}
>
{(
() => {
if (props.user.username) {
if (props.downloadInProgress === true) {
return 'Preparing download...';
}

return `${props.config.features.exportToWorkspace.downloadZipButtonText || 'Download Zip'}`;
}
return `Login to ${props.config.features.exportToWorkspace.downloadZipButtonText || 'Download Zip'}`;
}
)()}
</Button>
)
}
{ props.config.features.exportToWorkspace.enableDownloadManifest
&& (
<Popover
Expand All @@ -156,11 +306,12 @@ const DiscoveryActionBar = (props: Props) => {
: () => { handleRedirectToLoginClick(); }}
type='text'
disabled={props.selectedResources.length === 0}
icon={<DownloadOutlined />}
icon={<FileTextOutlined />}
>
{(props.user.username) ? `${props.config.features.exportToWorkspace.downloadManifestButtonText || 'Download Manifest'}`
: `Login to ${props.config.features.exportToWorkspace.downloadManifestButtonText || 'Download Manifest'}`}
</Button>

</Popover>
)}
<Popover
Expand Down Expand Up @@ -189,6 +340,28 @@ const DiscoveryActionBar = (props: Props) => {
{(props.user.username) ? 'Open In Workspace' : 'Login to Open In Workspace'}
</Button>
</Popover>
<Modal
closable={false}
visible={props.downloadStatusMessage.active}
title={props.downloadStatusMessage.title}
footer={(
<Button
onClick={
() => props.setDownloadStatusMessage({
title: '', message: '', active: false, url: '',
})
}
>
Close
</Button>
)}
>
{ props.downloadStatusMessage.message }
{
props.downloadStatusMessage.url
&& <a href={props.downloadStatusMessage.url}>{props.downloadStatusMessage.url}</a>
}
</Modal>
</Space>
)}
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/Discovery/DiscoveryConfig.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export interface DiscoveryConfig {
enableDownloadManifest: boolean
downloadManifestButtonText?: string
manifestFieldName: string
enableDownloadZip: boolean
downloadZipButtonText?: string
}
// explorationIntegration: {
// enabled: boolean // not supported
Expand Down
2 changes: 1 addition & 1 deletion src/Discovery/DiscoveryDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const DiscoveryDetails = (props: Props) => (
<Button
type='text'
onClick={() => {
navigator.clipboard.writeText(`${hostname}discovery/${props.modalData[props.config.minimalFieldMapping.uid]}/`)
navigator.clipboard.writeText(`${hostname}discovery/${encodeURIComponent(props.modalData[props.config.minimalFieldMapping.uid])}/`)
.then(() => {
props.setPermalinkCopied(true);
});
Expand Down