Skip to content

Commit

Permalink
JN-537 adding api wrapper utils (#535)
Browse files Browse the repository at this point in the history
  • Loading branch information
devonbush authored Sep 15, 2023
1 parent 5502960 commit 9571e05
Show file tree
Hide file tree
Showing 15 changed files with 173 additions and 178 deletions.
25 changes: 25 additions & 0 deletions ui-admin/src/api/api-utils.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { useState } from 'react'
import { useLoadingEffect } from './api-utils'
import { render, screen, waitFor } from '@testing-library/react'

const LoadingTestComponent = () => {
const [loadedList, setLoadedList] = useState<string[]>([])
const { isLoading } = useLoadingEffect(async () => {
const response = await Promise.resolve(['item1'])
setLoadedList(response)
})
return <div>
{!isLoading && <ul>
{loadedList.map(item => <li key={item}>{item}</li>)}
</ul>}
{isLoading && <span>LOADING</span>}
</div>
}

describe('useLoadingEffect handles loading state', () => {
it('manages isLoading', async () => {
render(<LoadingTestComponent/>)
expect(screen.getByText('LOADING')).toBeInTheDocument()
await waitFor(() => expect(screen.getByText('item1')).toBeInTheDocument())
})
})
70 changes: 70 additions & 0 deletions ui-admin/src/api/api-utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useEffect, useState } from 'react'
import { failureNotification } from 'util/notifications'
import { Store } from 'react-notifications-component'

export type ApiErrorResponse = {
message: string,
statusCode: number
}

const errorSuffix = 'If this error persists, please contact [email protected]'

/**
* performs default error message alerting if an error occurs during an API request.
* shows a specific error message if the error is auth-related.
*/
export const defaultApiErrorHandle = (error: ApiErrorResponse,
errorHeader = 'An unexpected error occurred. ') => {
if (error.statusCode === 401 || error.statusCode === 403) {
Store.addNotification(failureNotification(<div>
<div>{errorHeader}</div>
<div>Request could not be authorized
-- you may need to log in again </div>
<div>{errorSuffix}</div>
</div>
))
} else {
Store.addNotification(failureNotification(<div>
<div>{errorHeader}</div>
<div>{error.message}</div>
<div>{errorSuffix}</div>
</div>
))
}
}

/**
* utility effect for components that want to load something from the API on first render.
* returns loading and error state, as well as a function that can be called to reload.
*/
export const useLoadingEffect = (loadingFunc: () => Promise<unknown>,
deps: unknown[] = [], customErrorMsg?: string) => {
const [isLoading, setIsLoading] = useState(true)
const [isError, setIsError] = useState(false)
const reload = () => doApiLoad(loadingFunc, { setIsError, customErrorMsg, setIsLoading })

useEffect(() => {
reload()
}, deps)
return { isLoading, isError, reload }
}

/**
* utility function for wrapping an Api call in loading and error handling
*/
export const doApiLoad = async (loadingFunc: () => Promise<unknown>,
opts: {
setIsLoading?: (isLoading: boolean) => void,
setIsError?: (isError: boolean) => void,
customErrorMsg?: string
} = {}) => {
if (opts.setIsLoading) { opts.setIsLoading(true) }
try {
await loadingFunc()
if (opts.setIsError) { opts.setIsError(false) }
} catch (e) {
defaultApiErrorHandle(e as ApiErrorResponse, opts.customErrorMsg)
if (opts.setIsError) { opts.setIsError(true) }
}
if (opts.setIsLoading) { opts.setIsLoading(false) }
}
2 changes: 1 addition & 1 deletion ui-admin/src/api/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ export default {
if (response.ok) {
return obj
}
return Promise.reject(response)
return Promise.reject(obj)
},

async processResponse(response: Response) {
Expand Down
25 changes: 7 additions & 18 deletions ui-admin/src/portal/MailingListView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
import Api, { MailingListContact, PortalEnvironment } from 'api/api'
import { LoadedPortalContextT } from './PortalProvider'
import LoadingSpinner from 'util/LoadingSpinner'
Expand All @@ -16,13 +16,13 @@ import { escapeCsvValue, saveBlobAsDownload } from 'util/downloadUtils'
import { failureNotification, successNotification } from '../util/notifications'
import { Store } from 'react-notifications-component'
import Modal from 'react-bootstrap/Modal'
import { useLoadingEffect } from '../api/api-utils'


/** show the mailing list in table */
export default function MailingListView({ portalContext, portalEnv }:
{portalContext: LoadedPortalContextT, portalEnv: PortalEnvironment}) {
const [contacts, setContacts] = useState<MailingListContact[]>([])
const [isLoading, setIsLoading] = useState(true)
const [sorting, setSorting] = React.useState<SortingState>([])
const [rowSelection, setRowSelection] = React.useState<Record<string, boolean>>({})
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
Expand Down Expand Up @@ -83,17 +83,10 @@ export default function MailingListView({ portalContext, portalEnv }:
}
const numSelected = Object.keys(rowSelection).length

const loadMailingList = () => {
setIsLoading(true)
setRowSelection({})
Api.fetchMailingList(portalContext.portal.shortcode, portalEnv.environmentName).then(result => {
setContacts(result)
setIsLoading(false)
}).catch((e: Error) => {
alert(`error loading mailing list ${ e}`)
setIsLoading(false)
})
}
const { isLoading, reload } = useLoadingEffect(async () => {
const result = await Api.fetchMailingList(portalContext.portal.shortcode, portalEnv.environmentName)
setContacts(result)
}, [portalContext.portal.shortcode, portalEnv.environmentName])

const performDelete = async () => {
const contactsSelected = Object.keys(rowSelection)
Expand All @@ -111,14 +104,10 @@ export default function MailingListView({ portalContext, portalEnv }:
} catch {
Store.addNotification(failureNotification('Error: some entries could not be removed'))
}
loadMailingList() // just reload the whole thing to be safe
reload() // just reload the whole thing to be safe
setShowDeleteConfirm(false)
}

useEffect(() => {
loadMailingList()
}, [portalContext.portal.shortcode, portalEnv.environmentName])

return <div className="container p-3">
<h1 className="h4">Mailing list </h1>
<LoadingSpinner isLoading={isLoading}>
Expand Down
15 changes: 5 additions & 10 deletions ui-admin/src/study/kits/KitEnrolleeSelection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
import _keyBy from 'lodash/keyBy'
import _mapValues from 'lodash/mapValues'
import { Link } from 'react-router-dom'
Expand All @@ -18,6 +18,7 @@ import { basicTableLayout, checkboxColumnCell, ColumnVisibilityControl, Indeterm
import LoadingSpinner from 'util/LoadingSpinner'
import { instantToDateString } from 'util/timeUtils'
import RequestKitModal from '../participants/RequestKitModal'
import { useLoadingEffect } from 'api/api-utils'

type EnrolleeRow = Enrollee & {
taskCompletionStatus: Record<string, boolean>
Expand All @@ -28,7 +29,6 @@ type EnrolleeRow = Enrollee & {
*/
export default function KitEnrolleeSelection({ studyEnvContext }: { studyEnvContext: StudyEnvContextT }) {
const { portal, study, currentEnv, currentEnvPath } = studyEnvContext
const [isLoading, setIsLoading] = useState(true)
const [enrollees, setEnrollees] = useState<EnrolleeRow[]>([])
const [sorting, setSorting] = React.useState<SortingState>([
{ id: 'createdAt', desc: true },
Expand All @@ -44,8 +44,8 @@ export default function KitEnrolleeSelection({ studyEnvContext }: { studyEnvCont
])
const [showRequestKitModal, setShowRequestKitModal] = useState(false)

const loadEnrollees = async () => {
setIsLoading(true)

const { isLoading, reload } = useLoadingEffect(async () => {
const enrollees = await Api.fetchEnrolleesWithKits(
portal.shortcode, study.shortcode, currentEnv.environmentName)
const enrolleeRows = enrollees.map(enrollee => {
Expand All @@ -57,11 +57,6 @@ export default function KitEnrolleeSelection({ studyEnvContext }: { studyEnvCont
return { ...enrollee, taskCompletionStatus }
})
setEnrollees(enrolleeRows)
setIsLoading(false)
}

useEffect(() => {
loadEnrollees()
}, [studyEnvContext.study.shortcode, studyEnvContext.currentEnv.environmentName])

const onSubmit = async (kitType: string) => {
Expand All @@ -74,7 +69,7 @@ export default function KitEnrolleeSelection({ studyEnvContext }: { studyEnvCont
portal.shortcode, study.shortcode, currentEnv.environmentName, enrolleesSelected, kitType)

setShowRequestKitModal(false)
loadEnrollees()
reload()
}

const numSelected = Object.keys(rowSelection).length
Expand Down
15 changes: 4 additions & 11 deletions ui-admin/src/study/kits/KitList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
import _capitalize from 'lodash/capitalize'
import _fromPairs from 'lodash/fromPairs'
import _groupBy from 'lodash/groupBy'
Expand All @@ -13,6 +13,7 @@ import { StudyEnvContextT } from 'study/StudyEnvironmentRouter'
import LoadingSpinner from 'util/LoadingSpinner'
import { basicTableLayout, ColumnVisibilityControl } from 'util/tableUtils'
import { instantToDateString, isoToInstant } from 'util/timeUtils'
import { useLoadingEffect } from '../../api/api-utils'

type KitStatusTabConfig = {
status: string,
Expand Down Expand Up @@ -101,19 +102,11 @@ const pepperStatusToHumanStatus = (pepperStatus?: PepperKitStatus): string => {
/** Loads sample kits for a study and shows them as a list. */
export default function KitList({ studyEnvContext }: { studyEnvContext: StudyEnvContextT }) {
const { portal, study, currentEnv } = studyEnvContext
const [isLoading, setIsLoading] = useState(true)
const [kits, setKits] = useState<KitRequest[]>([])

const loadKits = async () => {
setIsLoading(true)
const kits = await Api.fetchKitsByStudyEnvironment(
portal.shortcode, study.shortcode, currentEnv.environmentName)
const { isLoading } = useLoadingEffect(async () => {
const kits= await Api.fetchKitsByStudyEnvironment(portal.shortcode, study.shortcode, currentEnv.environmentName)
setKits(kits)
setIsLoading(false)
}

useEffect(() => {
loadKits()
}, [studyEnvContext.study.shortcode, studyEnvContext.currentEnv.environmentName])

const kitsByStatus = _groupBy(kits, kit => {
Expand Down
28 changes: 11 additions & 17 deletions ui-admin/src/study/metrics/MetricGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
import { StudyEnvContextT } from '../StudyEnvironmentRouter'
import Api, { BasicMetricDatum } from '../../api/api'
import { Store } from 'react-notifications-component'
import { failureNotification } from '../../util/notifications'
import LoadingSpinner from '../../util/LoadingSpinner'
import Api, { BasicMetricDatum } from 'api/api'
import LoadingSpinner from 'util/LoadingSpinner'
import { cloneDeep } from 'lodash'
import { MetricInfo } from './StudyEnvMetricsView'
import Plot from 'react-plotly.js'
import { instantToDefaultString } from '../../util/timeUtils'
import { instantToDefaultString } from 'util/timeUtils'
import { useLoadingEffect } from 'api/api-utils'

const EXPORT_DELIMITER = '\t'

Expand All @@ -19,17 +18,12 @@ export default function MetricGraph({ studyEnvContext, metricInfo }: {studyEnvCo
metricInfo: MetricInfo}) {
const [metricData, setMetricData] = useState<BasicMetricDatum[] | null>(null)
const [plotlyTraces, setPlotlyTraces] = useState<PlotlyTimeTrace[] | null>(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
setIsLoading(true)
Api.fetchMetric(studyEnvContext.portal.shortcode, studyEnvContext.study.shortcode,
studyEnvContext.currentEnv.environmentName, metricInfo.name).then(result => {
setPlotlyTraces(makePlotlyTraces(result))
setMetricData(result)
setIsLoading(false)
}).catch(e => {
Store.addNotification(failureNotification(e.message))
})

const { isLoading } = useLoadingEffect(async () => {
const result = await Api.fetchMetric(studyEnvContext.portal.shortcode, studyEnvContext.study.shortcode,
studyEnvContext.currentEnv.environmentName, metricInfo.name)
setPlotlyTraces(makePlotlyTraces(result))
setMetricData(result)
}, [metricInfo.name, studyEnvContext.study.shortcode, studyEnvContext.currentEnv.environmentName])

const copyRawData = () => {
Expand Down
24 changes: 8 additions & 16 deletions ui-admin/src/study/participants/enrolleeView/EnrolleeLoader.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { StudyParams } from '../../StudyRouter'
import Api, { Enrollee } from 'api/api'
import { Store } from 'react-notifications-component'
import { failureNotification } from 'util/notifications'
import { StudyEnvContextT } from '../../StudyEnvironmentRouter'
import LoadingSpinner from 'util/LoadingSpinner'
import EnrolleeView from './EnrolleeView'
import { NavBreadcrumb } from 'navbar/AdminNavbar'
import { useLoadingEffect } from 'api/api-utils'

export type EnrolleeParams = StudyParams & {
enrolleeShortcode: string,
Expand All @@ -21,26 +20,19 @@ export default function EnrolleeLoader({ studyEnvContext }: {studyEnvContext: St
const params = useParams<EnrolleeParams>()
const enrolleeShortcode = params.enrolleeShortcode as string
const [enrollee, setEnrollee] = useState<Enrollee | null>(null)
const [isLoading, setIsLoading] = useState(true)

const loadEnrollee = () => {
Api.getEnrollee(portal.shortcode, study.shortcode, currentEnv.environmentName, enrolleeShortcode).then(result => {
setEnrollee(result)
setIsLoading(false)
}).catch(() => {
Store.addNotification(failureNotification(`Error loading participants`))
})
}

useEffect(() => {
loadEnrollee()
const { isLoading, reload } = useLoadingEffect(async () => {
const result = await Api.getEnrollee(
portal.shortcode, study.shortcode, currentEnv.environmentName, enrolleeShortcode
)
setEnrollee(result)
}, [enrolleeShortcode])

return <LoadingSpinner isLoading={isLoading}>
<NavBreadcrumb value={enrollee?.shortcode || ''}>
<Link to={`${currentEnvPath}/participants/${enrolleeShortcode}`}>
{enrollee?.shortcode}</Link>
</NavBreadcrumb>
<EnrolleeView enrollee={enrollee as Enrollee} studyEnvContext={studyEnvContext} onUpdate={loadEnrollee}/>
<EnrolleeView enrollee={enrollee as Enrollee} studyEnvContext={studyEnvContext} onUpdate={reload}/>
</LoadingSpinner>
}
Loading

0 comments on commit 9571e05

Please sign in to comment.