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 8 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
8 changes: 8 additions & 0 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 @@ -513,6 +517,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
153 changes: 151 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,97 @@ 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 = (err) => {
console.error(err);
setDownloadInProgress(false);
setDownloadStatusMessage({
title: 'Download failed',
message: 'There was a problem preparing your download.'
+ 'Please consider using the gen3 client 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;
const pollForJobStatusUpdate = () => {
fetchWithCreds({ path: `${jobAPIPath}status?UID=${uid}` }).then(
(statusResponse) => {
const { status } = statusResponse.data;
if (status === 'Failed') {
fetchWithCreds({ path: `${jobAPIPath}output?UID=${uid}` }).then(
(outputResponse) => {
setDownloadStatusMessage({
title: 'Download failed',
message: outputResponse.data.output,
active: true,
url: '',
});
setDownloadInProgress(false);
},
).catch(handleJobError);
} else if (status === 'Completed') {
fetchWithCreds({ path: `${jobAPIPath}output?UID=${uid}` }).then(
(outputResponse) => {
const { output } = outputResponse.data;
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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will cause the component stuck in an infinite loop if uid is not returned from https://github.com/uc-cdis/data-portal/pull/906/files#diff-1ac68dcc0067af6b35b142c058129318cc50ee20bfa9195afdb3d99f150d9d15R81

You can reproduce it now in qa-heal, since the mock auth is turned on, you will be login by default as a test account, which doesn't have sower access. The job dispatch request will return as 403 error, and no uid is returned, but this section of code will repeatly checking with uid=undefined

Better to handle fetched response with HTTP status code, rather than plain text

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. I will add some checks for HTTP status code for cases like this. For the /status calls, however, the status code from Sower is 200 regardless of job state, so I will have to keep some plain text checks in there

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense! probably only the first call to /job/dispatch needs a status code check

}
},
);
};
setTimeout(pollForJobStatusUpdate, 5000);
},
).catch(handleJobError);
};

const handleDownloadManifestClick = (config: DiscoveryConfig, selectedResources: any[]) => {
const { manifestFieldName } = config.features.exportToWorkspace;
if (!manifestFieldName) {
Expand Down Expand Up @@ -130,6 +221,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 +282,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 +316,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