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(mrf-admin-view): hawkeye v1.0 #7965

Open
wants to merge 35 commits into
base: develop
Choose a base branch
from
Open

Conversation

kevin9foong
Copy link
Contributor

@kevin9foong kevin9foong commented Dec 2, 2024

Problem

Currently, admins want a way to track the status of their multi-respondent submissions/workflows to see:

  • is the workflow completed / pending?
  • what is the current step of the workflow?

Closes FRM-1919

Solution

Add the above-mentioned information to the responses dashboard, csv download and individual responses page so admins can track the status.

Breaking Changes

  • No - this PR is backwards compatible

Before & After Screenshots

BEFORE:

image

AFTER:

image
New columns added and new name for timestamp column.

Tests

TC1: Hawk eye view dashboard shows the new status, current step and submission time of first step

  • Create 2 step MRF form without approval step
  • Submit step 1
  • Check responses dashboard, individual submission and download CSV. It should show pending status and correct step which is 1 of 2.
  • Submit step 2.
  • Check responses dashboard, individual submission and download CSV. It should show completed status and correct step which is 2 of 2.

TC2: Reject and approve show correctly.

  • Create a 3 step form with step 2 being approval step. Also, add a compulsory attachment field at step 1. When submitting the form in subsequent steps of the test case, always upload an attachment.
  • Submit step 1 and 2 of form, rejecting at step 2. (ie click No)
  • Check responses dashboard, individual submission and download CSV. It should show completed status and correct step which is 2 of 3.
  • Try download CSV with attachment. Ensure the csv file is correctly downloaded and the data in the csv is as expected and attachment files are downloaded.
  • Submit step 1, 2 and 3 of form, ensuring that step 2 is approve (ie click Yes)
  • Check responses dashboard, individual submission and download CSV. It should show completed status and correct step which is 3 of 3.
  • Try download CSV with attachment. Ensure the csv file is correctly downloaded and the data in the csv is as expected and attachment files are downloaded.

TC3: Storage mode and storage mode with with payments unaffected.

  • Create a storage mode form.
  • Submit and check the dashboard. Ensure status, current step and submission time of first step do not exist in the dashboard and the other expected columns exist (ie, same as before the changes introduced in this PR / refer to prod env)
  • Download the CSV, ensure that status, current step and submission time of first step do not exist in the csv.
  • Add payments to the storage mode form.
  • Submit and check the dashboard. Ensure status, current step and submission time of first step do not exist and the dashboard is same as previous (ie, same as before the changes introduced in this PR / refer to prod env).

@kevin9foong kevin9foong requested a review from KenLSM December 2, 2024 09:35
@kevin9foong kevin9foong self-assigned this Dec 2, 2024
@kevin9foong kevin9foong marked this pull request as draft December 2, 2024 09:35
@datadog-opengovsg
Copy link

datadog-opengovsg bot commented Dec 9, 2024

Datadog Report

Branch report: feat/hawkeye-v1.0
Commit report: 66b411c
Test service: formsg

✅ 0 Failed, 2150 Passed, 1 Skipped, 5m 6.71s Total duration (1m 39.78s time saved)

@kevin9foong
Copy link
Contributor Author

kevin9foong commented Dec 10, 2024

  • To reduce duplicate for the Step X of Y string into a common util function
  • To explore reusing types mrfMetadata


const SubmittedNonApprovalStep = z.object({
isApproval: z.literal(false),
submittedAt: z.string(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does zod have a timestamp type for validation? if so, maybe we can use it

Copy link
Contributor Author

@kevin9foong kevin9foong Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes - fixed referring to zod's datetime documentation with submittedAt: z.string().datetime({ precision: 3 })

@@ -0,0 +1,26 @@
import { WorkflowStatus } from '~shared/types'

/** Gets the business friendly string for current step of workflow. */
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are helper functions to generate the view for mrf submissions responses

define: {
// On local dev, global is undefined, causing decryption workers to fail.
// This is a workaround based on https://github.com/vitejs/vite/discussions/5912.
global: 'globalThis',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used the bypass an error faced as described by the comment.

Copy link
Contributor

@KenLSM KenLSM Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I experimented with removing the whole global access chunk in decryption.worker.ts and it seemed to have resolved the csv generation issue on local.

Might want to look into that solution since I would prefer deleting code than to have more config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed the global reference + vite config, tested and it works well!

/**
* Builds the metadata for a multirespondent form submission.
*/
export const buildMrfMetadata = ({
Copy link
Contributor Author

@kevin9foong kevin9foong Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mrfMetadata refers to data which is not stored but derived from stored data when buildMrfMetadata is executed. This metadata is used in the submission and submissionMetadata requests to display metadata info about the submission in the responses table, downloaded csv file and individual submission response detail page.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tested to generate the correct metadata given the submission data

@@ -112,7 +112,7 @@ export class EncryptedResponseCsvGenerator extends CsvGenerator {
if (this.hasBeenProcessed) return

// Create a header row in CSV using the fieldIdToQuestion map.
const headers = this.isMrf ? MRF_CSV_HEADERS : NON_MRF_CSV_HEADERS
const headers = this.isMrf ? [...MRF_CSV_HEADERS] : [...NON_MRF_CSV_HEADERS]
Copy link
Contributor Author

@kevin9foong kevin9foong Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: The de-structuring is necessary as we are mutating the headers array below. De-structuring creates a new array which prevents mutations from affecting the referenced const array (ie, MRF_CSV_HEADERS or NON_MRF_CSV_HEADERS).

Otherwise, headers points to the MRF_CSV_HEADERS or NON_MRF_CSV_HEADERS const array and mutates the same memory address which is shared across multiple runs of process(). This causes issues in cases where the memory is shared/reused,specifically test cases, which causes the headers to contain the headers from the previous runs of process() and for the test cases to fail.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good explanation to highlight that we need to create new array as we are mutating it at a later stage. Would recommend to put a short note in the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left a NOTE and TODO

@@ -193,7 +194,8 @@ const MRF_RESPONSE_TABLE_COLUMNS: Column<ResponseColumnData>[] = [
Header: MRF_FIRST_STEP_TIMESTAMP_LABEL,
accessor: 'submissionTime',
width: 250,
disableResizing: true
minWidth: 250,
Copy link
Contributor Author

@kevin9foong kevin9foong Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: re-using the same fix as existing storage mode responses table columns to include minWidth which allows the bar to expand for the rightmost column when the screen is small and horizontal scroll is enabled.

Copy link

linear bot commented Dec 13, 2024

@kevin9foong kevin9foong marked this pull request as ready for review December 13, 2024 17:23
</Badge>
)

const NON_MRF_RESPONSE_TABLE_COLUMNS: Column<ResponseColumnData>[] = [
Copy link
Contributor

@KenLSM KenLSM Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would prefer to use "base" or "default" rather than "non-MRF". As non-MRF is not an exception. i.e., Payment is special, MRF is special, but non-MRF is not special.

Suggested change
const NON_MRF_RESPONSE_TABLE_COLUMNS: Column<ResponseColumnData>[] = [
const BASE_RESPONSE_TABLE_COLUMNS: Column<ResponseColumnData>[] = [

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -138,6 +238,16 @@ export const ResponsesTable = () => {
}
}, [filteredMetadata, metadata, submissionId])

const columns = useMemo(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to see that this is extracted out to a function rather than choosing to add one more layer of ternary. 👍

return MRF_STATUS.COMPLETED
case WorkflowStatus.PENDING:
return MRF_STATUS.PENDING
default:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/nit since it is likely that all the WorkflowStatus is fully defined, we would want the typesystem to let us know that we need to update the cases here rather than it falling through to default.

Copy link
Contributor Author

@kevin9foong kevin9foong Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made it more explicit for undefined to become '' and explicitly specified return literals of the function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General notes on this generator. It has been awhile since I've looked at this file. I feel that using in-place mutation feels iffy. For instance, it can be difficult to follow the number of row.push(value) especially when it gets a little more complex as it is implicit rather than explicit.

Note that this comment is not regarding the changes that were introduced on this file, but I'm simply taking this opportunity to open up a discussion on potential refactors that we might want to consider in the future. The style of coding is likely an artifact of the past almost half a decade ago where FP is not as familiar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, ideally we would avoid mutation of the arrays as it can be hard to trace and debug. Will not introduce such a refactor in this PR to keep the scope defined to hawkeye.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a todo to document this desired change in the future

@@ -759,6 +791,7 @@ const buildSubmissionMetadata = (
result: MetadataAggregateResult,
currentNumber: number,
paymentMeta?: PaymentAggregates,
mrfMeta?: MultiRespondentAggregates,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would recommend to update the function definition to accept optional parameters as an object since I'm spotting a few uses of foo(paramX, paramY, null, paramZ).

const buildSubmissionMetadata = (
  result: MetadataAggregateResult,
  currentNumber: number,
  options?: { 
    paymentMeta?: PaymentAggregates,
    mrfMeta?: MultiRespondentAggregates,
  }
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored to utilise object argument

@@ -469,7 +478,6 @@ export const updateMultiRespondentFormSubmission = ({
encryptedPayload,
logMeta,
}: {
formId: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good spot 😆 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants