Skip to content
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
14 changes: 9 additions & 5 deletions src/pages/DataExport/form-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
DATE_AFTER_VALIDATOR,
} from '../../components/DatePicker/DatePickerField.jsx'
import {
locationAssign,
fetchAndDownload,
compressionToName,
pathToId,
} from '../../utils/helper.js'
Expand Down Expand Up @@ -44,11 +44,15 @@ const onExport = (baseUrl, setExportEnabled) => async (values) => {
const downloadUrlParams = valuesToParams(values)
const endpointExtension = values.format
const url = `${apiBaseUrl}${endpoint}.${endpointExtension}?${downloadUrlParams}`
locationAssign(url)
setExportEnabled(true)

// log for debugging purposes
console.log('data-export:', { url, params: downloadUrlParams })
try {
return await fetchAndDownload(url, 'data')
} finally {
setExportEnabled(true)

// log for debugging purposes
console.log('data-export:', { url, params: downloadUrlParams })
}
}

const validate = (values) => ({
Expand Down
41 changes: 4 additions & 37 deletions src/pages/EventExport/form-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,7 @@ import {
DATE_AFTER_VALIDATOR,
} from '../../components/DatePicker/DatePickerField.jsx'
import { ALL_VALUE } from '../../hooks/useProgramStages.js'
import { FORM_ERROR } from '../../utils/final-form.js'
import {
genericErrorMessage,
locationAssign,
pathToId,
} from '../../utils/helper.js'

const exportErrorAlert = (message) => ({
[FORM_ERROR]: [
{
id: `event-export-error-${Date.now()}`,
warning: true,
message,
},
],
})
import { fetchAndDownload, pathToId } from '../../utils/helper.js'

const onExport = (baseUrl, setExportEnabled) => async (values) => {
setExportEnabled(false)
Expand Down Expand Up @@ -61,30 +46,12 @@ const onExport = (baseUrl, setExportEnabled) => async (values) => {
const url = `${apiBaseUrl}${endpoint}.${endpointExtension}?${downloadUrlParams}`

try {
const response = await fetch(url, { credentials: 'include' })

if (!response.ok) {
let message = genericErrorMessage
try {
const body = await response.json()
message = body.message || message
} catch (e) {
// response body wasn't JSON, fall back to the generic message
console.error('event-export: failed to parse error response', e)
}
return exportErrorAlert(message)
}

const blob = await response.blob()
locationAssign(url, blob)
return await fetchAndDownload(url, 'event')
} finally {
setExportEnabled(true)

// log for debugging purposes
console.log('event-export:', { url, params: downloadUrlParams })
} catch (e) {
console.error('event-export: request failed', e)
return exportErrorAlert(genericErrorMessage)
} finally {
setExportEnabled(true)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
Objects,
SkipSharing,
ExportButton,
FormAlerts,
} from '../../components/Inputs/index.js'
import { onExport } from './form-helper.js'

Expand Down Expand Up @@ -56,8 +57,9 @@ const MetadataDependencyExport = () => {
initialValues={initialValues}
subscription={{
values: true,
submitError: true,
}}
render={({ handleSubmit, form, values }) => (
render={({ handleSubmit, form, values, submitError }) => (
<form onSubmit={handleSubmit}>
<ObjectType />
<Objects objectType={values.objectType} form={form} />
Expand All @@ -69,6 +71,7 @@ const MetadataDependencyExport = () => {
label={i18n.t('Export metadata dependencies')}
disabled={!exportEnabled}
/>
<FormAlerts alerts={submitError} />
</form>
)}
/>
Expand Down
22 changes: 13 additions & 9 deletions src/pages/MetadataDependencyExport/form-helper.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { locationAssign } from '../../utils/helper.js'
import { fetchAndDownload } from '../../utils/helper.js'

const onExport = (baseUrl, setExportEnabled) => (values) => {
const onExport = (baseUrl, setExportEnabled) => async (values) => {
setExportEnabled(false)

const { objectType, object, format, compression, skipSharing } = values
Expand All @@ -11,14 +11,18 @@ const onExport = (baseUrl, setExportEnabled) => (values) => {
const endpointExtension = compression ? `${format}.${compression}` : format
const downloadUrlParams = `skipSharing=${skipSharing}&download=true`
const url = `${apiBaseUrl}${endpoint}.${endpointExtension}?${downloadUrlParams}`
locationAssign(url)
setExportEnabled(true)

// log for debugging purposes
console.log('metadata-dependency-export:', {
url,
params: downloadUrlParams,
})
try {
return await fetchAndDownload(url, 'metadata-dependency')
} finally {
setExportEnabled(true)

// log for debugging purposes
console.log('metadata-dependency-export:', {
url,
params: downloadUrlParams,
})
}
}

export { onExport }
8 changes: 7 additions & 1 deletion src/pages/MetadataExport/MetadataExport.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
InclusionStrategy,
defaultInclusionStrategyOption,
ExportButton,
FormAlerts,
} from '../../components/Inputs/index.js'
import { onExport } from './form-helper.js'

Expand Down Expand Up @@ -56,7 +57,11 @@ const MetadataExport = () => {
<Form
onSubmit={onSubmit}
initialValues={initialValues}
render={({ handleSubmit }) => (
subscription={{
values: true,
submitError: true,
}}
render={({ handleSubmit, submitError }) => (
<form onSubmit={handleSubmit}>
<Schemas />
<Format availableFormats={formatNoXmlNoCsvOptions} />
Expand All @@ -70,6 +75,7 @@ const MetadataExport = () => {
label={i18n.t('Export metadata')}
disabled={!exportEnabled}
/>
<FormAlerts alerts={submitError} />
</form>
)}
/>
Expand Down
16 changes: 10 additions & 6 deletions src/pages/MetadataExport/form-helper.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { locationAssign } from '../../utils/helper.js'
import { fetchAndDownload } from '../../utils/helper.js'

const onExport = (baseUrl, setExportEnabled) => (values) => {
const onExport = (baseUrl, setExportEnabled) => async (values) => {
setExportEnabled(false)

const {
Expand All @@ -18,11 +18,15 @@ const onExport = (baseUrl, setExportEnabled) => (values) => {
const schemaParams = checkedSchemas.map((name) => `${name}=true`).join('&')
const downloadUrlParams = `skipSharing=${skipSharing}&inclusionStrategy=${inclusionStrategy}&download=true&${schemaParams}`
const url = `${apiBaseUrl}${endpoint}.${endpointExtension}?${downloadUrlParams}`
locationAssign(url)
setExportEnabled(true)

// log for debugging purposes
console.log('metadata-export:', { url, params: downloadUrlParams })
try {
return await fetchAndDownload(url, 'metadata')
} finally {
setExportEnabled(true)

// log for debugging purposes
console.log('metadata-export:', { url, params: downloadUrlParams })
}
}

export { onExport }
14 changes: 9 additions & 5 deletions src/pages/TEIExport/form-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
DATE_AFTER_VALIDATOR,
} from '../../components/DatePicker/DatePickerField.jsx'
import { OU_MODE_MANUAL_VALUE } from '../../components/Inputs/index.js'
import { locationAssign, pathToId } from '../../utils/helper.js'
import { fetchAndDownload, pathToId } from '../../utils/helper.js'

// calculate minimum set of parameters based on given filters
const valuesToParams = ({
Expand Down Expand Up @@ -112,11 +112,15 @@ const onExport = (baseUrl, setExportEnabled) => async (values) => {
const endpoint = `trackedEntities`
const downloadUrlParams = valuesToParams(values)
const url = `${apiBaseUrl}${endpoint}.${format}?${downloadUrlParams}`
locationAssign(url)
setExportEnabled(true)

// log for debugging purposes
console.log('tei-export:', { url, params: downloadUrlParams })
try {
return await fetchAndDownload(url, 'tei')
} finally {
setExportEnabled(true)

// log for debugging purposes
console.log('tei-export:', { url, params: downloadUrlParams })
}
}

const validate = (values) => {
Expand Down
42 changes: 42 additions & 0 deletions src/utils/helper.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import i18n from '@dhis2/d2-i18n'
import { FORM_ERROR } from './final-form.js'
import { getUploadXHR } from './xhr.js'

const trimString = (length, string) =>
Expand Down Expand Up @@ -214,6 +215,46 @@ const locationAssign = (url, blob) => {
}
}

const exportErrorAlert = (source, message) => ({
[FORM_ERROR]: [
{
id: `${source}-export-error-${Date.now()}`,
warning: true,
message,
},
],
})

// fetches the export URL, and only triggers the download if the server
// responded with a success status otherwise returns an error alert
const fetchAndDownload = async (url, source) => {
try {
const response = await fetch(url, { credentials: 'include' })

if (!response.ok) {
let message = genericErrorMessage
try {
const body = await response.json()
message = body.message || message
} catch (e) {
// response body wasn't JSON, fall back to the generic message
console.error(
`${source}-export: failed to parse error response`,
e
)
}
return exportErrorAlert(source, message)
}

const blob = await response.blob()
locationAssign(url, blob)
return undefined
} catch (e) {
console.error(`${source}-export: request failed`, e)
return exportErrorAlert(source, genericErrorMessage)
}
}

const getPrevJobDetails = (query, tasks) => {
if (!query || !query.id) {
return {}
Expand Down Expand Up @@ -245,6 +286,7 @@ const formatNumber = (value, locale) => {

export {
fetchAttributes,
fetchAndDownload,
getPrevJobDetails,
getInitialBoolValue,
formatNumber,
Expand Down
64 changes: 63 additions & 1 deletion src/utils/helper.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { formatNumber, locationAssign } from './helper.js'
import { FORM_ERROR } from './final-form.js'
import { fetchAndDownload, formatNumber, locationAssign } from './helper.js'

describe('formatNumber', () => {
it('adds digit group separators to numbers', () => {
Expand Down Expand Up @@ -79,3 +80,64 @@ describe('locationAssign', () => {
expect(link.download).toEqual('events.json.zip')
})
})

describe('fetchAndDownload', () => {
const url = 'https://debug.dhis2.org/dev/api/metadata.json'

afterEach(() => {
global.fetch.mockRestore?.()
delete global.fetch
})

it('downloads the file and returns no error when the response is ok', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
blob: () => Promise.resolve(new Blob(['{}'])),
})

const result = await fetchAndDownload(url, 'metadata')

expect(global.fetch).toHaveBeenCalledWith(url, {
credentials: 'include',
})
expect(result).toBeUndefined()
})

it('returns a form error with the server message when the response is not ok', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: false,
json: () => Promise.resolve({ message: 'Conflict occurred' }),
})

const result = await fetchAndDownload(url, 'metadata')

expect(result[FORM_ERROR]).toHaveLength(1)
expect(result[FORM_ERROR][0]).toMatchObject({
warning: true,
message: 'Conflict occurred',
})
})

it('falls back to a generic message when the error response has no JSON body', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: false,
json: () => Promise.reject(new Error('not json')),
})

const result = await fetchAndDownload(url, 'metadata')

expect(result[FORM_ERROR][0].message).toEqual(
'An unknown error occurred. Please try again later'
)
})

it('returns a form error when the request itself fails', async () => {
global.fetch = jest.fn().mockRejectedValue(new Error('network down'))

const result = await fetchAndDownload(url, 'metadata')

expect(result[FORM_ERROR][0].message).toEqual(
'An unknown error occurred. Please try again later'
)
})
})
Loading