diff --git a/frontend/src/components/CrossSearch/CrossSearchExportMenuItem.tsx b/frontend/src/components/CrossSearch/CrossSearchExportMenuItem.tsx index 65146f3b..60bc8eda 100644 --- a/frontend/src/components/CrossSearch/CrossSearchExportMenuItem.tsx +++ b/frontend/src/components/CrossSearch/CrossSearchExportMenuItem.tsx @@ -9,46 +9,65 @@ import { currentDateAsString } from '@/shared/currentDateAsString' export const CrossSearchExportMenuItem = ({ handleClose }: { handleClose: () => void }) => { const [loading, setLoading] = useState(false) const { sqlColumnFilters, sqlOrderBy } = usePageContext() - const notify = useNotify() + const { notify, setMessage } = useNotify() const token = useUser().token const fetchOptions = token ? { headers: { Authorization: `Bearer ${token}` } } : {} const filename = `cross_search${currentDateAsString()}.csv` - const fetchCSVFile = () => { + const fetchCSVFile = async () => { setLoading(true) - notify('Downloading CSV file, please wait...', 'info', null) const URIColumnFilters = encodeURIComponent(JSON.stringify(sqlColumnFilters)) const URIOrderBy = encodeURIComponent(JSON.stringify(sqlOrderBy)) - fetch(`${BACKEND_URL}/crosssearch/export/${URIColumnFilters}/${URIOrderBy}`, fetchOptions) - .then(response => { - if (!response.ok) { - throw new Error('Server response was not OK.') - } - return response.blob() - }) - .then(blob => { - const blobUrl = window.URL.createObjectURL(blob) - const downloadLink = document.createElement('a') - downloadLink.href = blobUrl - downloadLink.download = filename - document.body.appendChild(downloadLink) - downloadLink.click() - downloadLink.remove() - window.URL.revokeObjectURL(blobUrl) - notify('Download finished.') - }) - .catch(_ => { - notify('Downloading cross-search export failed.', 'error') - }) - .finally(() => setLoading(false)) + notify('Generating CSV file, please wait...', 'info', null) + const response = await fetch(`${BACKEND_URL}/crosssearch/export/${URIColumnFilters}/${URIOrderBy}`, fetchOptions) + + if (!response.ok) { + throw new Error('Server response was not OK.') + } + const reader = response.body!.getReader() + const file: Uint8Array[] = [] + let bytes = 0 + let closed = false + + const showDownloadProgress = () => { + if (!closed) { + setTimeout(() => { + setMessage(`Downloading CSV file, ${Math.round((bytes / 1000000) * 10) / 10} MB`) + showDownloadProgress() + }, 500) + } + } + showDownloadProgress() + while (true) { + const { done, value } = await reader.read() + if (done) break + bytes = bytes + value.length + file.push(value) + } + closed = true + try { + const blobUrl = window.URL.createObjectURL(new Blob(file)) + const downloadLink = document.createElement('a') + downloadLink.href = blobUrl + downloadLink.download = filename + document.body.appendChild(downloadLink) + downloadLink.click() + downloadLink.remove() + window.URL.revokeObjectURL(blobUrl) + notify('Download finished.') + } catch { + notify('Downloading cross-search export failed.', 'error') + } finally { + setLoading(false) + } } return ( { - fetchCSVFile() + void fetchCSVFile() handleClose() }} disabled={loading} diff --git a/frontend/src/components/Notification.tsx b/frontend/src/components/Notification.tsx index e12abea6..b5c90a6d 100755 --- a/frontend/src/components/Notification.tsx +++ b/frontend/src/components/Notification.tsx @@ -8,6 +8,7 @@ export type NotificationContext = { open: boolean setOpen: (open: boolean) => void message: string + setMessage: (message: string) => void severity?: Severity timeoutValue?: number | null } @@ -36,7 +37,7 @@ export const NotificationContextProvider = ({ children }: { children: ReactNode }, 200) } return ( - + {children} ) diff --git a/frontend/src/hooks/notification.ts b/frontend/src/hooks/notification.ts index 50e3335e..30d76aaa 100755 --- a/frontend/src/hooks/notification.ts +++ b/frontend/src/hooks/notification.ts @@ -2,6 +2,6 @@ import { NotificationContext } from '@/components/Notification' import { useContext } from 'react' export const useNotify = () => { - const { notify } = useContext(NotificationContext) - return notify + const { notify, setMessage } = useContext(NotificationContext) + return { notify, setMessage } }