Skip to content

Commit

Permalink
Feat/bulk/all get job client (#2093)
Browse files Browse the repository at this point in the history
* feat: add sqs client and service

* feat: create job and job items and sendSqsmessage in bulk controller

* chore: move qrCode job creation into JobController

* chore: generalize sqs message sending

* chore: add sqs timeout

* chore: fix missing region

* chore: create qrBatchSize and sqsRegion env variable

* chore: fix error logging and type

* chore: fix bulk tests

* chore: fix lambda params

* chore: add JobItemId to job_items model for easy update

* chore: refactor logic to update jobItemStatus

* feat: expose endpoint to update jobItem

* chore: use env var for lambda hash value

* chore: use http post request to send completion callback

* chore: fix validation

* chore: add tests

* chore: fix error catching for lambda

* chore: add status to job table

* chore: add logic to update job after job item callback

* chore: fix tests

* chore: fix tests

* fix: call next to pass control

* fix: add env variable to feature flag job creation

* chore: add repository methods to retrieve jobs

* chore: add service methods for long polling and retrieving job information

* chore: add endpoints for users to retrieve their latest job and updated job status

* chore: add tests

* fix: add env variables to docker-compose

* chore: add env variable for bulk download bucket

* fix: env variable naming in docker-compose

* chore: fix tests

* chore: fix tests

* fix: status bar should appear even if no urls

* chore: pass download urls to Download QR code button

* chore: add callback variable to actions and reducers

* chore: remove redundant bulkQRCodesStarted action

* chore: add client-side logic to retrieve user job

* chore: add component logic for status bar

* chore: fix end to end tests

* fix: uncomment sqs.sendService function

* chore: update api call on client for job update

Co-authored-by: Jim Engay <e0425954@u.nus.edu>
  • Loading branch information
gweiying and jimvae committed Nov 28, 2022
1 parent 5b35d05 commit 0f98c28
Show file tree
Hide file tree
Showing 12 changed files with 181 additions and 71 deletions.
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ services:
- JOB_POLL_ATTEMPTS=12
- JOB_POLL_INTERVAL=5000
- BULK_QR_CODE_BUCKET_URL=

volumes:
- ./public:/usr/src/gogovsg/public
- ./src:/usr/src/gogovsg/src
Expand Down
135 changes: 110 additions & 25 deletions src/client/user/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,14 @@ const closeStatusBar: () => CloseStatusBarAction = () => ({
const setStatusBarErrorMessage: (
header: string,
body: string,
) => SetStatusBarErrorMessageAction = (header: string, body: string) => ({
callbacks: string[],
) => SetStatusBarErrorMessageAction = (
header: string,
body: string,
callbacks: string[],
) => ({
type: UserAction.SET_STATUS_BAR_ERROR_MESSAGE,
payload: { header, body },
payload: { header, body, callbacks },
})

const setStatusBarInfoMessage: (
Expand All @@ -264,9 +269,14 @@ const setStatusBarInfoMessage: (
const setStatusBarSuccessMessage: (
header: string,
body: string,
) => SetStatusBarSuccessMessageAction = (header: string, body: string) => ({
callbacks: string[],
) => SetStatusBarSuccessMessageAction = (
header: string,
body: string,
callbacks: string[],
) => ({
type: UserAction.SET_STATUS_BAR_SUCCESS_MESSAGE,
payload: { header, body },
payload: { header, body, callbacks },
})

// retrieve linkHistory based on query object
Expand Down Expand Up @@ -645,25 +655,6 @@ const urlCreated = (
)
}

const bulkQRCodesStarted = (
dispatch: ThunkDispatch<
GoGovReduxState,
void,
| CloseCreateUrlModalAction
| ResetUserStateAction
| SetStatusBarInfoMessageAction
>,
fileName: string,
) => {
dispatch<void>(getUrlsForUser())
dispatch<ResetUserStateAction>(resetUserState())

const header = `QR code creation from ${fileName} file is in progress.`
const body = `We will notify you via email once it is completed.`

dispatch<SetStatusBarInfoMessageAction>(setStatusBarInfoMessage(header, body))
}

/**
* API call to create URL
* If user is not logged in, the createUrl call returns unauthorized,
Expand Down Expand Up @@ -934,6 +925,90 @@ const updateTags =
})
}

const setQRCodeGenerationMessage =
(status: string, callbacks: string[]) =>
(
dispatch: ThunkDispatch<
GoGovReduxState,
void,
| SetStatusBarSuccessMessageAction
| SetStatusBarErrorMessageAction
| SetStatusBarInfoMessageAction
>,
) => {
if (status === 'SUCCESS') {
const header = `QR codes successfully generated`
const body = `Please download your QR codes here or via email`
dispatch<SetStatusBarSuccessMessageAction>(
setStatusBarSuccessMessage(header, body, callbacks),
)
}
if (status === 'IN_PROGRESS') {
const header = `QR codes generation in progress`
const body = `Please wait to download your QR codes`
dispatch<SetStatusBarInfoMessageAction>(
setStatusBarInfoMessage(header, body),
)
}
if (status === 'FAILED') {
const header = `QR codes failed to generate`
const body = `Please try again`
dispatch<SetStatusBarErrorMessageAction>(
setStatusBarErrorMessage(header, body, callbacks),
)
}
}

const getJobInformationForPendingJobId =
(jobId: number) =>
async (
dispatch: ThunkDispatch<
GoGovReduxState,
void,
SetStatusBarSuccessMessageAction | SetStatusBarErrorMessageAction
>,
) => {
const query = querystring.stringify({ jobId })
const response = await get(`/api/user/job/status?${query}`)
const isOk = response.ok
if (!isOk) {
// retry
dispatch(getJobInformationForPendingJobId(jobId))
} else {
const resp = await response.json()
const { job, jobItemUrls } = resp
const { status } = job
dispatch(setQRCodeGenerationMessage(status, jobItemUrls))
}
}

const getLatestJob =
() =>
async (
dispatch: ThunkDispatch<
GoGovReduxState,
void,
| SetStatusBarSuccessMessageAction
| SetStatusBarErrorMessageAction
| SetStatusBarInfoMessageAction
>,
) => {
const response = await get(`/api/user/job/latest`)
const isOk = response.ok
const resp = await response.json()
if (!isOk) {
throw new Error(resp.message || 'Error fetching user jobs ')
}

const { job, jobItemUrls } = resp
if (!job || !jobItemUrls) return
const { status, id } = job
if (status === 'IN_PROGRESS') {
dispatch(getJobInformationForPendingJobId(id))
}
dispatch(setQRCodeGenerationMessage(status, jobItemUrls))
}

/**
* API call to upload file for bulk creation.
* @param file
Expand All @@ -949,8 +1024,11 @@ const bulkCreateUrl =
| ResetUserStateAction
| SetInfoMessageAction
| SetErrorMessageAction
| SetSuccessMessageAction
| SetIsUploadingAction
| SetFileUploadStateAction
| CloseCreateUrlModalAction
| ResetUserStateAction
>,
getState: GetReduxState,
) => {
Expand Down Expand Up @@ -1028,8 +1106,14 @@ const bulkCreateUrl =
} else {
Sentry.captureMessage('bulk creation success')
GAEvent('modal page', 'bulk creation', 'successful')
await response.json()
bulkQRCodesStarted(dispatch, file.name)
dispatch<void>(getUrlsForUser())
dispatch<ResetUserStateAction>(resetUserState())

const { count, job } = await response.json()
dispatch<SetSuccessMessageAction>(
rootActions.setSuccessMessage(`${count} links have been created`),
)
if (job) dispatch<void>(getLatestJob())
dispatch<SetFileUploadStateAction>(setFileUploadState(true))
}
}
Expand Down Expand Up @@ -1073,4 +1157,5 @@ export default {
setUrlUploadState,
setTags,
updateTags,
getLatestJob,
}
2 changes: 2 additions & 0 deletions src/client/user/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export type SetStatusBarErrorMessageAction = ReduxPayloadAction<
{
header: string
body: string
callbacks: string[]
}
>

Expand All @@ -207,6 +208,7 @@ export type SetStatusBarSuccessMessageAction = ReduxPayloadAction<
{
header: string
body: string
callbacks: string[]
}
>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,55 @@ import * as Sentry from '@sentry/react'
import { GAEvent } from '../../../../app/util/ga'
import { DropdownButton, DropdownOption } from '../../../widgets/DropdownButton'

// TODO: add bucket path, ideally we want to store this as an env variable and import it
const BUCKET_PATH = ''

type DownloadFormats = 'csv' | 'png' | 'svg'

type DownloadPathMap = { [format in DownloadFormats]: string }

const downloadPaths: DownloadPathMap = {
csv: 'urls.csv',
png: 'qr_code_png.zip',
svg: 'qr_code_svg.zip',
csv: 'generated.csv',
png: 'generated_png.zip',
svg: 'generated_svg.zip',
}

function downloadFileFromS3(
format: DownloadFormats,
bulkCsvId: string,
bucketPath: string = BUCKET_PATH,
) {
// TODO: exact functionality to be confirmed
try {
window.open(`${bucketPath}/${bulkCsvId}/${downloadPaths[format]}`)
GAEvent(`qr code bulk download`, format, 'successful')
} catch (e) {
Sentry.captureMessage(`qr code bulk download for ${format} unsuccessful`)
GAEvent('qr code bulk download', format, 'unsuccesful')
}
function downloadFileFromS3(format: DownloadFormats, bulkCsvIds: string[]) {
bulkCsvIds.forEach((bulkCsvId) => {
try {
window.open(`${bulkCsvId}/${downloadPaths[format]}`)
GAEvent(`qr code bulk download`, format, 'successful')
} catch (e) {
Sentry.captureMessage(`qr code bulk download for ${format} unsuccessful`)
GAEvent('qr code bulk download', format, 'unsuccesful')
}
})
}

export default function DownloadBulkButton() {
const bulkCsvId = 'placeholder_id' // TODO: likely will be retrieved from Redux store
const DownloadBulkButton = ({ bulkCsvIds }: { bulkCsvIds: string[] }) => {
const options: DropdownOption[] = [
{
name: 'Download all',
onClick: () => {
downloadFileFromS3('csv', bulkCsvId)
downloadFileFromS3('png', bulkCsvId)
downloadFileFromS3('svg', bulkCsvId)
downloadFileFromS3('csv', bulkCsvIds)
downloadFileFromS3('png', bulkCsvIds)
downloadFileFromS3('svg', bulkCsvIds)
},
},
{
name: 'Links (CSV File)',
onClick: () => downloadFileFromS3('csv', bulkCsvId),
onClick: () => downloadFileFromS3('csv', bulkCsvIds),
},
{
name: 'QR Codes (PNG)',
onClick: () => downloadFileFromS3('png', bulkCsvId),
onClick: () => downloadFileFromS3('png', bulkCsvIds),
},
{
name: 'QR Codes (SVG)',
onClick: () => downloadFileFromS3('svg', bulkCsvId),
onClick: () => downloadFileFromS3('svg', bulkCsvIds),
},
]

return (
<DropdownButton buttonText="Download .zip" options={options} fullWidth />
)
}

export default DownloadBulkButton
47 changes: 35 additions & 12 deletions src/client/user/components/UserLinkTable/StatusBar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Typography, createStyles, makeStyles } from '@material-ui/core'
import { Alert, AlertTitle } from '@material-ui/lab'
Expand Down Expand Up @@ -57,16 +57,12 @@ const useStyles = makeStyles((theme) =>
const StatusBar = () => {
const appMargins = useAppMargins()
const classes = useStyles({ appMargins })
const [showStatusBar, setShowStatusBar] = useState<boolean>(true)

const header = useSelector(
(state: GoGovReduxState) => state.user.statusBarMessage.header,
)
const body = useSelector(
(state: GoGovReduxState) => state.user.statusBarMessage.body,
)
const variant = useSelector(
(state: GoGovReduxState) => state.user.statusBarMessage.variant,
const statusBarMessage = useSelector(
(state: GoGovReduxState) => state.user.statusBarMessage,
)
const { header, body, callbacks, variant } = statusBarMessage

const hasStatusBarAlert = !!(header || body)
const dispatch = useDispatch()
Expand All @@ -92,18 +88,45 @@ const StatusBar = () => {

const dispatchCloseStatusBar = () => {
dispatch(userActions.closeStatusBar())
// store last acknowledged message
localStorage.setItem('statusBarMessage', JSON.stringify(statusBarMessage))
}

const getLatestJob = () => dispatch(userActions.getLatestJob())

useEffect(() => {
getLatestJob()
}, [])

useEffect(() => {
if (header && body) {
const localStatusBarMessage = localStorage.getItem('statusBarMessage')
const statusBarMessageString = JSON.stringify(statusBarMessage)
if (localStatusBarMessage === statusBarMessageString) {
// if current message is same as last acknowledged message, hide
setShowStatusBar(false)
} else {
// current message has not been acknowledged before
setShowStatusBar(true)
}
}
}, [header, body])

return (
<>
{hasStatusBarAlert && (
{showStatusBar && hasStatusBarAlert && (
<Alert
icon={icon}
className={`${colorClass} ${classes.content}`}
action={
<>
{variant === StatusBarVariant.Success && <DownloadBulkButton />}
<CloseButton onClick={dispatchCloseStatusBar} />
{variant === StatusBarVariant.Success &&
callbacks.length >= 0 && (
<DownloadBulkButton bulkCsvIds={callbacks} />
)}
{variant !== StatusBarVariant.Info && (
<CloseButton onClick={dispatchCloseStatusBar} />
)}
</>
}
>
Expand Down
2 changes: 0 additions & 2 deletions src/client/user/components/UserLinkTable/UrlTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import MemoTablePagination from './MemoTablePagination'
import userActions from '../../../actions'
import { GoGovReduxState } from '../../../../app/reducers/types'
import { UrlTableConfig } from '../../../reducers/types'
import StatusBar from '../StatusBar'

const useStyles = makeStyles((theme) =>
createStyles({
Expand All @@ -32,7 +31,6 @@ const UrlTable = () => {

return (
<div className={classes.urlTable}>
<StatusBar />
<Table aria-label="table with urls">
<EnhancedTableBody />
</Table>
Expand Down

0 comments on commit 0f98c28

Please sign in to comment.