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

feat: database upload modal rather than multiple upload buttons #164

Merged
merged 8 commits into from
Jul 20, 2023
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,13 @@ skaffold dev

## Notes about fixed dependency versions

- `openapi-typescript@5.4.1` is used because later versions do not work with OpenAPI V2, which our backend is stuck on
- `axios-jwt` is fixed to `3.0.0` because the latest version at the time of writing (13-07-2023) has [a bug](https://github.com/jetbridge/axios-jwt/issues/57). We should be able to upgrade once this is fixed.
- `openapi-typescript@5.4.1` is used because later versions do not work with OpenAPI V2, which our backend is stuck on
- `axios-jwt` is fixed to `3.0.0` because the latest version at the time of writing (13-07-2023)
has [a bug](https://github.com/jetbridge/axios-jwt/issues/57). We should be able to upgrade once this is fixed.

## TODO

* Validate create group form
* Can submit without hostname... No effect, no error
* Loading shows within form
* Show selected file after selecting database to upload
10 changes: 5 additions & 5 deletions src/views/databases/databases-list.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
ButtonStrip,
DataTable,
DataTableBody as TableBody,
DataTableCell,
DataTableColumnHeader,
DataTableRow,
DataTableBody as TableBody,
DataTableHead as TableHead,
DataTableRow,
DataTableToolbar as TableToolbar,
} from '@dhis2/ui'
import Moment from 'react-moment'
Expand All @@ -14,8 +14,8 @@ import { GroupWithDatabases } from '../../types'
import { DeleteButton } from './delete-button'
import { DownloadButton } from './download-button'
import styles from './databases-list.module.css'
import { UploadButton } from './upload-button'
import type { FC } from 'react'
import { UploadButton } from './upload-button'

export const DatabasesList: FC = () => {
const [{ data }, refetch] = useAuthAxios<GroupWithDatabases[]>('databases', {
Expand All @@ -25,14 +25,14 @@ export const DatabasesList: FC = () => {
return (
<div className={styles.wrapper}>
<div className={styles.heading}>
<h1>All databases</h1>
<h1>Databases</h1>
<UploadButton onComplete={refetch} />
</div>

{data?.map((group) => (
<div key={group.name}>
<TableToolbar className={styles.tabletoolbar}>
<h2>{group.name}</h2>
<UploadButton groupName={group.name} onComplete={refetch} />
</TableToolbar>
<DataTable>
<TableHead>
Expand Down
30 changes: 0 additions & 30 deletions src/views/databases/upload-button.module.css

This file was deleted.

84 changes: 19 additions & 65 deletions src/views/databases/upload-button.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,26 @@
import { useAlert } from '@dhis2/app-service-alerts'
import { FileInput, LinearLoader } from '@dhis2/ui'
import { useCallback, useState } from 'react'
import { useAuthAxios } from '../../hooks'
import { GroupWithDatabases } from '../../types'
import styles from './upload-button.module.css'
import { Button, IconAdd24 } from '@dhis2/ui'
import type { FC } from 'react'
import { useState } from 'react'
import { UploadDatabaseModal } from './upload-database-modal'

type UploadButtonProps = { groupName: string; onComplete: Function }
type UploadButtonProps = {
onComplete: Function
}

export const UploadButton: FC<UploadButtonProps> = ({ groupName, onComplete }) => {
const { show: showAlert } = useAlert(
({ message }) => message,
({ isCritical }) => (isCritical ? { critical: true } : { success: true })
)
const [uploadProgress, setUploadProgress] = useState(0)
const [fileName, setFileName] = useState('')
const onUploadProgress = useCallback((progressEvent) => {
const { loaded, total } = progressEvent
const percentage = Math.floor((loaded * 100) / total)
setUploadProgress(percentage)
}, [])
const [{ loading }, postDatabase, cancelPostRequest] = useAuthAxios<GroupWithDatabases>(
{
url: `/databases`,
method: 'post',
onUploadProgress,
},
{ manual: true }
)
const onFileSelect = useCallback(
async ({ files }) => {
try {
const file = files[0]
const formData = new FormData()
formData.append('group', groupName)
formData.append('database', file, file.name)
setFileName(file.name)
await postDatabase({ data: formData })
showAlert({
message: 'Database added successfully',
isCritical: false,
})
onComplete()
} catch (error) {
console.error(error)
showAlert({
message: 'There was a problem uploading the database',
isCritical: true,
})
}
},
[groupName, onComplete, postDatabase, showAlert]
)
export const UploadButton: FC<UploadButtonProps> = ({ onComplete }) => {
const [showModal, setShowModal] = useState<boolean>(false)

const complete = () => {
setShowModal(false)
onComplete()
}

return (
<div className={styles.container}>
<FileInput buttonLabel="Upload a database" onChange={onFileSelect} disabled={loading} />
{loading && (
<div className={styles.progressWrap}>
<span className={styles.label}>
Uploading database <b>{fileName}</b> ({uploadProgress}%)
<button className={styles.cancelButton} onClick={cancelPostRequest}>
Cancel
</button>
</span>
<LinearLoader amount={uploadProgress} />
</div>
)}
</div>
<>
<Button icon={<IconAdd24 />} onClick={() => setShowModal(true)}>
Upload database
</Button>
{showModal && <UploadDatabaseModal onClose={() => setShowModal(false)} onComplete={complete} />}
</>
)
}
29 changes: 29 additions & 0 deletions src/views/databases/upload-database-modal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.heading {
display: flex;
align-items: center;
color: var(--colors-grey900);
gap: var(--spacers-dp16);
}
.heading h1 {
font-weight: 400;
font-size: 28px;
line-height: 40px;
}
.wrapper {
width: 100%;
}
.tabletoolbar {
background-color: var(--colors-white);
}
.opendeletewrap {
min-width: 180px;
display: flex;
gap: var(--spacers-dp8);
}

.loader {
position: fixed;
top: 50%;
left: 50%;
z-index: 1000;
}
142 changes: 142 additions & 0 deletions src/views/databases/upload-database-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {
Button,
ButtonStrip,
Center,
CircularLoader,
FileInput,
LinearLoader,
Modal,
ModalActions,
ModalContent,
ModalTitle,
SingleSelectField,
SingleSelectOption,
} from '@dhis2/ui'
import styles from './upload-database-modal.module.css'
import type { FC } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useAuthAxios } from '../../hooks'
import { useAlert } from '@dhis2/app-service-alerts'
import { Group, GroupWithDatabases } from '../../types'

type UploadDatabaseModalProps = {
onClose: Function
onComplete: Function
}

export const UploadDatabaseModal: FC<UploadDatabaseModalProps> = ({ onClose, onComplete }) => {
const [group, setGroup] = useState('')
const [databaseFile, setDatabaseFile] = useState(new Blob())

const { show: showAlert } = useAlert(
({ message }) => message,
({ isCritical }) => (isCritical ? { critical: true } : { success: true })
)

const [uploadProgress, setUploadProgress] = useState(0)

const onUploadProgress = useCallback((progressEvent) => {
const { loaded, total } = progressEvent
const percentage = Math.floor((loaded * 100) / total)
setUploadProgress(percentage)
}, [])

const [{ loading }, postDatabase, cancelPostRequest] = useAuthAxios<GroupWithDatabases>(
{
url: `/databases`,
method: 'post',
onUploadProgress,
},
{ manual: true }
)

const onUpload = useCallback(async () => {
// TODO: How do I disable the upload button until a file is selected
if (databaseFile.size === 0) {
showAlert({
message: 'No file selected',
isCritical: true,
})
return
}

try {
const formData = new FormData()
formData.append('group', group)
formData.append('database', databaseFile, databaseFile.name)
await postDatabase({ data: formData })
showAlert({
message: 'Database added successfully',
isCritical: false,
})
onComplete()
} catch (error) {
showAlert({
message: 'There was a problem uploading the database',
isCritical: true,
})
console.error(error)
}
}, [databaseFile, group, onComplete, postDatabase, showAlert])

const [{ data: groups, loading: groupsLoading, error: groupsError }] = useAuthAxios<Group[]>({
method: 'GET',
url: '/groups',
})

if (groupsError) {
showAlert({ message: 'There was a problem loading the groups', isCritical: true })
console.error(groupsError)
}
const onFileSelect = useCallback(async ({ files }) => setDatabaseFile(files[0]), [])

useEffect(() => {
if (groups && groups.length > 0) {
setGroup(groups[0].name)
}
}, [groups])

if (groupsLoading) {
return
}

return (
<Modal fluid onClose={onClose}>
<ModalTitle>Upload database</ModalTitle>
<ModalContent>
<SingleSelectField className={styles.field} selected={group} filterable={true} onChange={({ selected }) => setGroup(selected)} label="Group">
{groups.map((group) => (
<SingleSelectOption key={group.name} label={group.name} value={group.name} />
))}
</SingleSelectField>
<FileInput buttonLabel="Select database" onChange={onFileSelect} disabled={loading} />
{loading && (
<div className={styles.progressWrap}>
<span className={styles.label}>
Uploading database <b>{databaseFile.name}</b> ({uploadProgress}%)
<button className={styles.cancelButton} onClick={cancelPostRequest}>
Cancel
</button>
</span>
<LinearLoader amount={uploadProgress} />
</div>
)}
</ModalContent>
<ModalActions>
<ButtonStrip end>
<Button onClick={onUpload} disabled={loading}>
Upload
</Button>
<Button onClick={onClose}>
Close
</Button>
</ButtonStrip>
</ModalActions>
{loading && (
<Center>
<CircularLoader />
</Center>
)}
</Modal>
)
}
Loading