From 57930c470116532e2e4c10ebb721af92e390ac9f Mon Sep 17 00:00:00 2001 From: Theo Sanderson Date: Sun, 17 Mar 2024 17:57:51 +0000 Subject: [PATCH 1/9] wip --- .../src/components/ReviewPage/ReviewPage.tsx | 66 +++++++++++++++---- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/website/src/components/ReviewPage/ReviewPage.tsx b/website/src/components/ReviewPage/ReviewPage.tsx index 5ecba016bd..86b878c2b5 100644 --- a/website/src/components/ReviewPage/ReviewPage.tsx +++ b/website/src/components/ReviewPage/ReviewPage.tsx @@ -15,6 +15,7 @@ import { awaitingApprovalForRevocationStatus, type PageQuery, type SequenceEntryStatus, + receivedStatus, } from '../../types/backend.ts'; import { type ClientConfig } from '../../types/runtimeConfig.ts'; import { displayConfirmationDialog } from '../ConfirmationDialog.tsx'; @@ -34,9 +35,30 @@ type ReviewPageProps = { const pageSizeOptions = [10, 20, 50, 100] as const; +const NumberAndVisibility = ({ text, number, setVisibility, visibilityEnabled }) => +{ + // checkbox number, text + return
+ +
+ + + + +} + const InnerReviewPage: FC = ({ clientConfig, organism, accessToken }) => { const { errorMessage, isErrorOpen, openErrorFeedback, closeErrorFeedback } = useErrorFeedbackState(); const [showErrors, setShowErrors] = useState(true); + const [showUnprocessed, setShowUnprocessed] = useState(true); + const [showValid, setShowValid] = useState(false); const [pageQuery, setPageQuery] = useState({ page: 1, size: pageSizeOptions[2] }); const hooks = useSubmissionOperations(organism, clientConfig, accessToken, openErrorFeedback, pageQuery); @@ -66,8 +88,36 @@ const InnerReviewPage: FC = ({ clientConfig, organism, accessTo const processedCount = hooks.getSequences.data.statusCounts[awaitingApprovalStatus]; const errorCount = hooks.getSequences.data.statusCounts[hasErrorsStatus]; const revocationCount = hooks.getSequences.data.statusCounts[awaitingApprovalForRevocationStatus]; + const receivedCount = hooks.getSequences.data.statusCounts[receivedStatus]; + const finishedCount = processedCount + errorCount + revocationCount; + const unfinishedCount = receivedCount + processingCount; + const validCount = processedCount + revocationCount; + + const categoryInfo = [ + { + text: "sequences still awaiting processing", + number: unfinishedCount, + setVisibility: setShowUnprocessed, + visibilityEnabled: showUnprocessed, + }, + { + text: "valid sequences", + number: validCount, + setVisibility: setShowValid, + visibilityEnabled: showValid, + }, + { + text: "sequences with errors", + number: errorCount, + setVisibility: setShowErrors, + visibilityEnabled: showErrors, + } + + ] + + const sequences: SequenceEntryStatus[] = hooks.getSequences.data.sequenceEntries; @@ -78,14 +128,11 @@ const InnerReviewPage: FC = ({ clientConfig, organism, accessTo {processingCount > 0 && }
- setShowErrors(e.target.checked)} - /> - Also show entries with errors + { + categoryInfo.map((info) => { + return + }) + }
); @@ -193,9 +240,6 @@ const InnerReviewPage: FC = ({ clientConfig, organism, accessTo const reviewCards = (
{sequences.map((sequence) => { - if (!showErrors && sequence.status === hasErrorsStatus) { - return null; - } return (
Date: Sun, 17 Mar 2024 20:25:19 +0000 Subject: [PATCH 2/9] Many improvements --- .../submission/SubmissionDatabaseService.kt | 21 +- .../components/ReviewPage/ReviewPage.spec.tsx | 4 +- .../src/components/ReviewPage/ReviewPage.tsx | 184 ++++++++++++------ website/src/hooks/useSubmissionOperations.ts | 10 +- website/tests/pages/review/review.page.ts | 6 +- 5 files changed, 145 insertions(+), 80 deletions(-) diff --git a/backend/src/main/kotlin/org/loculus/backend/service/submission/SubmissionDatabaseService.kt b/backend/src/main/kotlin/org/loculus/backend/service/submission/SubmissionDatabaseService.kt index 7cf96a78e6..aef7f8cb5d 100644 --- a/backend/src/main/kotlin/org/loculus/backend/service/submission/SubmissionDatabaseService.kt +++ b/backend/src/main/kotlin/org/loculus/backend/service/submission/SubmissionDatabaseService.kt @@ -433,7 +433,7 @@ class SubmissionDatabaseService( val listOfStatuses = statusesFilter ?: Status.entries sequenceEntriesTableProvider.get(organism).let { table -> - val query = table + val baseQuery = table .join( DataUseTermsTable, JoinType.LEFT, @@ -456,30 +456,33 @@ class SubmissionDatabaseService( ) .select( where = { - table.statusIsOneOf(listOfStatuses) and - table.groupNameIsOneOf(validatedGroupNames) + table.groupNameIsOneOf(validatedGroupNames) }, ) .orderBy(table.accessionColumn) if (organism != null) { - query.andWhere { table.organismIs(organism) } + baseQuery.andWhere { table.organismIs(organism) } } - val statusCounts: Map = listOfStatuses.associateWith { status -> - query.count { it[table.statusColumn] == status.name } + val statusCounts: Map = Status.entries.associateWith { status -> + baseQuery.count { it[table.statusColumn] == status.name } + } + + val filteredQuery = baseQuery.andWhere { + table.statusIsOneOf(listOfStatuses) } if (warningsFilter == WarningsFilter.EXCLUDE_WARNINGS) { - query.andWhere { + filteredQuery.andWhere { not(table.entriesWithWarnings) } } val pagedQuery = if (page != null && size != null) { - query.limit(size, (page * size).toLong()) + filteredQuery.limit(size, (page * size).toLong()) } else { - query + filteredQuery } return GetSequenceResponse( diff --git a/website/src/components/ReviewPage/ReviewPage.spec.tsx b/website/src/components/ReviewPage/ReviewPage.spec.tsx index f86b31ae50..c792bc41e9 100644 --- a/website/src/components/ReviewPage/ReviewPage.spec.tsx +++ b/website/src/components/ReviewPage/ReviewPage.spec.tsx @@ -83,7 +83,7 @@ describe('ReviewPage', () => { const { getByText } = renderReviewPage(); await waitFor(() => { - expect(getByText('No sequences to review')).toBeDefined(); + expect(getByText('You do not currently have any unreleased sequences')).toBeDefined(); }); }); @@ -132,7 +132,7 @@ describe('ReviewPage', () => { getByText((text) => text.includes('Confirm')).click(); await waitFor(() => { - expect(getByText('No sequences to review')).toBeDefined(); + expect(getByText('You do not currently have any unreleased sequences')).toBeDefined(); }); }); diff --git a/website/src/components/ReviewPage/ReviewPage.tsx b/website/src/components/ReviewPage/ReviewPage.tsx index 86b878c2b5..e4df8617f8 100644 --- a/website/src/components/ReviewPage/ReviewPage.tsx +++ b/website/src/components/ReviewPage/ReviewPage.tsx @@ -15,6 +15,7 @@ import { awaitingApprovalForRevocationStatus, type PageQuery, type SequenceEntryStatus, + type GetSequencesResponse, receivedStatus, } from '../../types/backend.ts'; import { type ClientConfig } from '../../types/runtimeConfig.ts'; @@ -23,9 +24,12 @@ import { ManagedErrorFeedback, useErrorFeedbackState } from '../common/ManagedEr import { withQueryProvider } from '../common/withQueryProvider.tsx'; import BiTrash from '~icons/bi/trash'; import IwwaArrowDown from '~icons/iwwa/arrow-down'; +import MdiEye from '~icons/mdi/eye'; import WpfPaperPlane from '~icons/wpf/paper-plane'; const menuItemClassName = `group flex rounded-md items-center w-full px-2 py-2 text-sm -hover:bg-gray-400 bg-gray-500 text-white text-left mb-1`; +hover:bg-primary-500 bg-primary-600 text-white text-left mb-1`; + +let oldSequenceData: GetSequencesResponse | null = null; type ReviewPageProps = { clientConfig: ClientConfig; @@ -35,104 +39,152 @@ type ReviewPageProps = { const pageSizeOptions = [10, 20, 50, 100] as const; -const NumberAndVisibility = ({ text, number, setVisibility, visibilityEnabled }) => -{ +const NumberAndVisibility = ({ + text, + countNumber, + setVisibility, + visibilityEnabled, +}: { + text: string; + countNumber: number; + setVisibility: (value: boolean) => void; + visibilityEnabled: boolean; +}) => { // checkbox number, text - return
-
+ ); +}; - type='checkbox' - checked={visibilityEnabled} - onChange={() => setVisibility(!visibilityEnabled)} - className='mr-2' - /> - {number} {text} -
+const InnerReviewPage: FC = ({ clientConfig, organism, accessToken }) => { + const { errorMessage, isErrorOpen, openErrorFeedback, closeErrorFeedback } = useErrorFeedbackState(); + const [pageQuery, setPageQuery] = useState({ page: 1, size: pageSizeOptions[2] }); + const hooks = useSubmissionOperations(organism, clientConfig, accessToken, openErrorFeedback, pageQuery); + const showErrors = hooks.includedStatuses.includes(hasErrorsStatus); + const showUnprocessed = + hooks.includedStatuses.includes(inProcessingStatus) && hooks.includedStatuses.includes(receivedStatus); + const showValid = + hooks.includedStatuses.includes(awaitingApprovalStatus) && + hooks.includedStatuses.includes(awaitingApprovalForRevocationStatus); -} + const setAStatus = (status: string, value: boolean) => { + hooks.setIncludedStatuses((prev) => { + if (value) { + return [...prev, status]; + } + return prev.filter((s) => s !== status); + }); + }; -const InnerReviewPage: FC = ({ clientConfig, organism, accessToken }) => { - const { errorMessage, isErrorOpen, openErrorFeedback, closeErrorFeedback } = useErrorFeedbackState(); - const [showErrors, setShowErrors] = useState(true); - const [showUnprocessed, setShowUnprocessed] = useState(true); - const [showValid, setShowValid] = useState(false); - const [pageQuery, setPageQuery] = useState({ page: 1, size: pageSizeOptions[2] }); + const setShowErrors = (value: boolean) => setAStatus(hasErrorsStatus, value); + const setShowUnprocessed = (value: boolean) => { + setAStatus(inProcessingStatus, value); + setAStatus(receivedStatus, value); + }; - const hooks = useSubmissionOperations(organism, clientConfig, accessToken, openErrorFeedback, pageQuery); + const setShowValid = (value: boolean) => { + setAStatus(awaitingApprovalStatus, value); + setAStatus(awaitingApprovalForRevocationStatus, value); + }; const handleSizeChange = (event: ChangeEvent) => { const newSize = parseInt(event.target.value, 10); setPageQuery({ page: 1, size: newSize }); }; + let sequencesData = hooks.getSequences.data; + + if (!hooks.getSequences.isLoading && !hooks.getSequences.isError) { + oldSequenceData = hooks.getSequences.data; + } + if (hooks.getSequences.isLoading) { - return
Loading...
; + // Type checking doesn't seem to understand how to handle the ref, so we need to.. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions + if (oldSequenceData) { + sequencesData = oldSequenceData; + } else { + return
Loading...
; + } } if (hooks.getSequences.isError) { return
Error: {hooks.getSequences.error.message}
; } - - if (hooks.getSequences.data.sequenceEntries.length === 0) { - return
No sequences to review
; + if (sequencesData === undefined) { + return
Loading..
; + // this is not expected to happen, but it's here to satisfy the type checker } - const total = Object.values(hooks.getSequences.data.statusCounts).reduce( - (acc: number, count: number) => acc + count, - 0, - ); - const processingCount = hooks.getSequences.data.statusCounts[inProcessingStatus]; - const processedCount = hooks.getSequences.data.statusCounts[awaitingApprovalStatus]; - const errorCount = hooks.getSequences.data.statusCounts[hasErrorsStatus]; - const revocationCount = hooks.getSequences.data.statusCounts[awaitingApprovalForRevocationStatus]; - const receivedCount = hooks.getSequences.data.statusCounts[receivedStatus]; - + const total = Object.values(sequencesData.statusCounts).reduce((acc: number, count: number) => acc + count, 0); + const processingCount = sequencesData.statusCounts[inProcessingStatus]; + const processedCount = sequencesData.statusCounts[awaitingApprovalStatus]; + const errorCount = sequencesData.statusCounts[hasErrorsStatus]; + const revocationCount = sequencesData.statusCounts[awaitingApprovalForRevocationStatus]; + const receivedCount = sequencesData.statusCounts[receivedStatus]; const finishedCount = processedCount + errorCount + revocationCount; const unfinishedCount = receivedCount + processingCount; const validCount = processedCount + revocationCount; + if (total === 0) { + return ( +
+ You do not currently have any unreleased sequences awaiting review. +
+ ); + } + const categoryInfo = [ { - text: "sequences still awaiting processing", - number: unfinishedCount, + text: 'sequences still awaiting processing', + countNumber: unfinishedCount, setVisibility: setShowUnprocessed, visibilityEnabled: showUnprocessed, }, { - text: "valid sequences", - number: validCount, + text: 'valid sequences', + countNumber: validCount, setVisibility: setShowValid, visibilityEnabled: showValid, }, { - text: "sequences with errors", - number: errorCount, + text: 'sequences with errors', + countNumber: errorCount, setVisibility: setShowErrors, visibilityEnabled: showErrors, - } - - ] - - + }, + ]; - const sequences: SequenceEntryStatus[] = hooks.getSequences.data.sequenceEntries; + const sequences: SequenceEntryStatus[] = sequencesData.sequenceEntries; const controlPanel = ( -
-
- {finishedCount} of {total} sequences processed. - {processingCount > 0 && } +
+
+ {unfinishedCount > 0 && ( + + )} + {finishedCount} of {total} sequences processed
-
- { - categoryInfo.map((info) => { - return - }) - } +
+ + {categoryInfo.map((info) => { + return ; + })}
); @@ -163,10 +215,10 @@ const InnerReviewPage: FC = ({ clientConfig, organism, accessTo ); const bulkActionButtons = ( -
+
{finishedCount > 0 && ( - - + + Discard sequences @@ -218,7 +270,7 @@ const InnerReviewPage: FC = ({ clientConfig, organism, accessTo )} {processedCount + revocationCount > 0 && (