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: Invalidate upload #385 #450

Merged
merged 8 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
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}`,
tab: 'N/A',
as1729 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading