Skip to content

Commit

Permalink
Feat: Invalidate upload #385 (#450)
Browse files Browse the repository at this point in the history
* add upload invalidation

* add loading status

* correct title

* include email

---------

Co-authored-by: aditya <[email protected]>
  • Loading branch information
greg-adams and as1729 authored Sep 30, 2024
1 parent a038d3f commit b8dc826
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "UploadValidation" ADD COLUMN "isManual" BOOLEAN NOT NULL DEFAULT false;
5 changes: 3 additions & 2 deletions api/db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,12 @@ model Upload {
}

model UploadValidation {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
uploadId Int
upload Upload @relation(fields: [uploadId], references: [id], onDelete: Cascade)
upload Upload @relation(fields: [uploadId], references: [id], onDelete: Cascade)
results Json? @db.JsonB
passed Boolean
isManual Boolean @default(false)
initiatedById Int
initiatedBy User @relation(fields: [initiatedById], references: [id])
createdAt DateTime @default(now()) @db.Timestamptz(6)
Expand Down
3 changes: 3 additions & 0 deletions api/src/graphql/uploadValidations.sdl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const schema = gql`
upload: Upload!
results: JSON
passed: Boolean!
isManual: Boolean!
initiatedById: Int!
initiatedBy: User!
createdAt: DateTime!
Expand All @@ -22,13 +23,15 @@ export const schema = gql`
uploadId: Int!
results: JSON
passed: Boolean!
isManual: Boolean
initiatedById: Int!
}
input UpdateUploadValidationInput {
uploadId: Int
results: JSON
passed: Boolean
isManual: Boolean
initiatedById: Int
}
Expand Down
1 change: 1 addition & 0 deletions api/src/services/uploads/uploads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const createUpload: MutationResolvers['createUpload'] = async ({
uploadId: upload.id,
initiatedById: upload.uploadedById,
passed: false,
isManual: false,
results: null,
},
})
Expand Down
67 changes: 63 additions & 4 deletions web/src/components/Upload/Upload/Upload.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
import { useEffect } from 'react'
import { useEffect, useState } from 'react'

import { useAuth } from 'web/src/auth'

import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'

import { timeTag } from 'src/lib/formatters'

import UploadValidationButtonGroup from '../UploadValidationButtonGroup/UploadValidationButtonGroup'
import UploadValidationResultsTable from '../UploadValidationResultsTable/UploadValidationResultsTable'
import UploadValidationResultsTable, {
Severity,
} from '../UploadValidationResultsTable/UploadValidationResultsTable'
import UploadValidationStatus from '../UploadValidationStatus/UploadValidationStatus'

const DOWNLOAD_UPLOAD_FILE = gql`
mutation downloadUploadFile($id: Int!) {
downloadUploadFile(id: $id)
}
`

const CREATE_VALIDATION_MUTATION = gql`
mutation CreateUploadValidationMutation(
$input: CreateUploadValidationInput!
) {
createUploadValidation(input: $input) {
initiatedById
passed
isManual
results
uploadId
}
}
`

const Upload = ({ upload, queryResult }) => {
const { startPolling, stopPolling } = queryResult
const { currentUser } = useAuth()
const { startPolling, stopPolling, refetch } = queryResult
const hasErrors =
upload.latestValidation?.results?.errors !== null &&
Array.isArray(upload.latestValidation?.results?.errors) &&
upload.latestValidation?.results?.errors.length > 0
const [savingUpload, setSavingUpload] = useState(false)

useEffect(() => {
const pollingTimeout = 120 * 1000
Expand Down Expand Up @@ -55,8 +77,44 @@ const Upload = ({ upload, queryResult }) => {
downloadUploadFile({ variables: { id: upload.id } })
}

const [createValidation] = useMutation(CREATE_VALIDATION_MUTATION, {
onCompleted: async () => {
toast.success('Upload invalidated')
await refetch()
setSavingUpload(false)
},
onError: (error) => {
toast.error(error.message)
setSavingUpload(false)
},
})

const handleValidate = () => {}
const handleForceInvalidate = () => {}

const handleForceInvalidate = async () => {
setSavingUpload(true)
const invalidateResult = [
{
message: `Manually invalidated by User: ${currentUser.name} (${currentUser.email})`,
tab: 'N/A',
row: 'N/A',
col: 'N/A',
severity: Severity.Error,
},
]

const inputManual = {
initiatedById: currentUser.id,
passed: false,
isManual: true,
results: {
errors: invalidateResult,
},
uploadId: upload.id,
}

await createValidation({ variables: { input: inputManual } })
}

return (
<>
Expand Down Expand Up @@ -104,6 +162,7 @@ const Upload = ({ upload, queryResult }) => {
handleValidate={handleValidate}
handleForceInvalidate={handleForceInvalidate}
handleFileDownload={handleFileDownload}
savingUpload={savingUpload}
/>
</>
)}
Expand Down
1 change: 1 addition & 0 deletions web/src/components/Upload/UploadCell/UploadCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const QUERY = gql`
latestValidation {
id
passed
isManual
results
createdAt
initiatedBy {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
import { ROLES } from 'api/src/lib/constants'
import Button from 'react-bootstrap/Button'
import { useAuth } from 'web/src/auth'

import { Severity } from 'src/components/Upload/UploadValidationResultsTable/UploadValidationResultsTable'

interface ValidationError {
message: string
tab?: string
row?: string
col?: string
severity: 'warn' | 'err' | 'info'
severity: Severity
}

interface ValidationResult {
errors: ValidationError[]
}

interface UploadValidationButtonGroupProps {
latestValidation?: { passed: boolean; results: ValidationResult | null }
latestValidation?: {
passed: boolean
isManual: boolean
results: ValidationResult | null
}
handleFileDownload: () => void
handleForceInvalidate: () => void
handleValidate: () => void
savingUpload: boolean
}

const UploadValidationButtonGroup = ({
latestValidation,
handleFileDownload,
handleForceInvalidate,
handleValidate,
savingUpload,
}: UploadValidationButtonGroupProps) => {
const { hasRole } = useAuth()

/*
If the upload has been validated, renders "Invalidate" and "Re-validate" buttons
If the upload has been invalidated, renders the "Validate" button
Expand All @@ -43,13 +46,14 @@ const UploadValidationButtonGroup = ({
variant="outline-primary"
size="sm"
onClick={handleForceInvalidate}
disabled={savingUpload}
>
Invalidate
</Button>
)}{' '}
<Button variant="primary" size="sm" onClick={handleValidate}>
{passed ? 'Re-Validate' : 'Validate'}
</Button>
{/* <Button variant="primary" size="sm" onClick={handleValidate}>
{passed || manuallyInvalidated ? 'Re-Validate' : 'Validate'}
</Button> */}
</>
)
}
Expand All @@ -60,10 +64,7 @@ const UploadValidationButtonGroup = ({
<Button variant="primary" size="sm" onClick={handleFileDownload}>
Download file
</Button>{' '}
{/* TODO: Remove USDR_ADMIN check when ready || 2024-05-13 Milestone */}
{hasRole(ROLES.USDR_ADMIN) &&
latestValidation?.results &&
renderValidationButtons()}
{latestValidation?.results && renderValidationButtons()}
</div>
</li>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,45 @@ const UploadValidationStatus = ({ latestValidation }) => {
}

const getValidationStatus = () => {
if (latestValidation?.results === null) {
const {
passed,
isManual,
createdAt,
results,
initiatedBy: { name },
} = latestValidation

if (results === null) {
return (
<span className="text-warning fst-italic">
Validation in progress...
</span>
)
}

const { passed, createdAt, initiatedBy } = latestValidation
const statusText = passed ? 'Validated' : 'Did not pass validation'
const statusClass = passed ? 'text-success' : 'text-danger'
const valueClassName = !passed ? 'text-danger' : 'text-success'
const time = timeTag(createdAt)

let statusOutcome
if (isManual) {
statusOutcome = 'Invalidated'
} else {
statusOutcome = passed ? 'Validated' : 'Did not pass validation'
}

const statusText = (
<>
{statusOutcome}
{' on '}
{time}
{' by '}
{name}
</>
)

return (
<span className={statusClass}>
{getValidationIcon(passed)} {statusText} on {timeTag(createdAt)} by{' '}
{initiatedBy?.name}
<span className={valueClassName}>
{getValidationIcon(passed)} {statusText}
</span>
)
}
Expand Down
17 changes: 9 additions & 8 deletions web/src/components/Upload/Uploads/columns.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table'
import type { Upload } from 'types/graphql'

import { Link, routes } from '@redwoodjs/router'

Expand All @@ -22,20 +23,20 @@ function valueAsLink(cell): JSX.Element {

function validationDisplay(row) {
const { latestValidation } = row
const { results, passed, isManual, createdAt } = latestValidation

if (!latestValidation || latestValidation.results === null) {
if (!latestValidation || results === null) {
return 'Not set'
}

const { passed, createdAt } = latestValidation
const formattedDate = formatDateString(createdAt)

if (!passed) {
return (
<span className="text-danger">
Did not pass validation on {formattedDate}
</span>
)
const invalidText = isManual
? `Invalidated on ${formattedDate}`
: `Did not pass validation on ${formattedDate}`

return <span className="text-danger">{invalidText}</span>
}

return formattedDate
Expand All @@ -50,7 +51,7 @@ export const columnDefs = [
cell: (info) => info.getValue(),
header: 'Agency',
}),
columnHelper.accessor('expenditureCategory.code', {
columnHelper.accessor((row: Upload) => row.expenditureCategory?.code, {
cell: (info) => info.getValue() ?? 'Not set',
header: 'EC Code',
}),
Expand Down
1 change: 1 addition & 0 deletions web/src/components/Upload/UploadsCell/UploadsCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const QUERY = gql`
id
createdAt
passed
isManual
results
}
createdAt
Expand Down
8 changes: 4 additions & 4 deletions web/src/components/User/Users/Users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ const UsersList = ({ usersByOrganization, updateUser, usersUpdating }) => {
? user.name
: user.name + ' (Deactivated)'
const deactivateTitle = user.isActive
? 'Deactivate user ' + user.id
: 'Reactivate user ' + user.id
? 'Deactivate user ' + user.name
: 'Reactivate user ' + user.name
const deactivateLabel = user.isActive ? 'Deactivate' : 'Reactivate'

// Actions
Expand All @@ -48,11 +48,11 @@ const UsersList = ({ usersByOrganization, updateUser, usersUpdating }) => {
<td>{truncate(user.agency?.name)}</td>
<td>{timeTag(user.createdAt)}</td>
<td>
<div className="d-grid gap-2 d-xl-block">
<div className="d-grid gap-2 d-xxl-block">
{hasEditAccess && (
<Link
to={routes.editUser({ id: user.id })}
className="btn btn-secondary btn-sm me-xl-2"
className="btn btn-secondary btn-sm me-xxl-2"
title={'Edit user ' + user.id}
>
Edit
Expand Down

0 comments on commit b8dc826

Please sign in to comment.