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

wip: Seq page revocation #1239

Merged
merged 19 commits into from
Mar 9, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ class SubmissionDatabaseService(
accessionPreconditionValidator.validateAccessionVersions(
submitter,
accessionVersionsFilter,
listOf(AWAITING_APPROVAL),
listOf(AWAITING_APPROVAL, AWAITING_APPROVAL_FOR_REVOCATION),
organism,
)
}
Expand All @@ -255,7 +255,7 @@ class SubmissionDatabaseService(
(DataUseTermsTable.isNewestDataUseTerms)
},
).select {
val statusCondition = table.statusIs(AWAITING_APPROVAL)
val statusCondition = table.statusIsOneOf(listOf(AWAITING_APPROVAL, AWAITING_APPROVAL_FOR_REVOCATION))

val accessionCondition = if (accessionVersionsFilter !== null) {
table.accessionVersionIsIn(accessionVersionsFilter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.loculus.backend.api.AccessionVersion
import org.loculus.backend.api.ApproveDataScope
import org.loculus.backend.api.Status.APPROVED_FOR_RELEASE
import org.loculus.backend.api.Status.AWAITING_APPROVAL
import org.loculus.backend.api.Status.AWAITING_APPROVAL_FOR_REVOCATION
import org.loculus.backend.api.Status.IN_PROCESSING
import org.loculus.backend.controller.DEFAULT_ORGANISM
import org.loculus.backend.controller.EndpointTest
Expand Down Expand Up @@ -156,7 +157,8 @@ class ApproveProcessedDataEndpointTest(
jsonPath(
"$.detail",
containsString(
"Accession versions are in not in one of the states [$AWAITING_APPROVAL]: " +
"Accession versions are in not in one of the states " +
"[$AWAITING_APPROVAL, $AWAITING_APPROVAL_FOR_REVOCATION]: " +
"${accessionVersionNotInCorrectState.first().displayAccessionVersion()} - $IN_PROCESSING",
),
),
Expand Down
13 changes: 13 additions & 0 deletions website/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"react-chartjs-2": "^5.2.0",
"react-confirm-alert": "^3.0.6",
"react-dom": "^18.2.0",
"react-toastify": "^10.0.4",
"react-tooltip": "^5.26.3",
"unplugin-icons": "^0.18.5",
"winston": "^3.11.0",
Expand Down
8 changes: 5 additions & 3 deletions website/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,19 @@ export default defineConfig({
globalSetup: './tests/playwrightSetup.ts',

projects: [

{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
...process.env.ALL_BROWSERS === 'true' ? [{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
}] : [],

],
});
});
68 changes: 44 additions & 24 deletions website/src/components/ReviewPage/ReviewCard.tsx
theosanderson marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Tooltip } from 'react-tooltip';
import { backendClientHooks } from '../../services/serviceHooks.ts';
import {
awaitingApprovalStatus,
awaitingApprovalForRevocationStatus,
type DataUseTerms,
hasErrorsStatus,
inProcessingStatus,
Expand Down Expand Up @@ -65,6 +66,14 @@ export const ReviewCard: FC<ReviewCardProps> = ({
value={sequenceEntryStatus.submissionId}
/>
{data !== undefined && <MetadataList data={data} isLoading={isLoading} />}
{sequenceEntryStatus.status === awaitingApprovalForRevocationStatus && (
<KeyValueComponent
accessionVersion={getAccessionVersionString(sequenceEntryStatus)}
keyName='Revocation entry'
value='This is a revocation entry, which will create a new version that revokes this accession'
extraStyle='text-red-600 font-semibold'
/>
)}
</div>
<ButtonBar
sequenceEntryStatus={sequenceEntryStatus}
Expand Down Expand Up @@ -105,39 +114,47 @@ const ButtonBar: FC<ButtonBarProps> = ({
return (
<div className='flex space-x-1 mb-auto pt-3.5'>
<button
className={buttonBarClass(sequenceEntryStatus.status !== awaitingApprovalStatus)}
className={buttonBarClass(
sequenceEntryStatus.status !== awaitingApprovalStatus &&
sequenceEntryStatus.status !== awaitingApprovalForRevocationStatus,
)}
onClick={approveAccessionVersion}
data-tooltip-id={'approve-tooltip' + sequenceEntryStatus.accession}
disabled={sequenceEntryStatus.status !== awaitingApprovalStatus}
disabled={
sequenceEntryStatus.status !== awaitingApprovalStatus &&
sequenceEntryStatus.status !== awaitingApprovalForRevocationStatus
}
>
<WpfPaperPlane />
</button>
<Tooltip
id={'approve-tooltip' + sequenceEntryStatus.accession}
content={
sequenceEntryStatus.status === awaitingApprovalStatus
sequenceEntryStatus.status === awaitingApprovalStatus ||
sequenceEntryStatus.status === awaitingApprovalForRevocationStatus
? 'Release this sequence entry'
: sequenceEntryStatus.status === hasErrorsStatus
? 'Cannot release. Fix Errors!'
: 'Cannot release. Wait for preprocessing!'
? 'You need to fix the errors before releasing this sequence entry'
: 'Still awaiting preprocessing'
}
/>

<button
className={buttonBarClass(
sequenceEntryStatus.status !== hasErrorsStatus &&
sequenceEntryStatus.status !== awaitingApprovalStatus,
)}
data-testid={`${getAccessionVersionString({ ...sequenceEntryStatus })}.edit`}
data-tooltip-id={'edit-tooltip' + sequenceEntryStatus.accession}
onClick={editAccessionVersion}
disabled={
sequenceEntryStatus.status !== hasErrorsStatus &&
sequenceEntryStatus.status !== awaitingApprovalStatus
}
>
<ClarityNoteEditLine />
</button>
{sequenceEntryStatus.status !== awaitingApprovalForRevocationStatus && (
<button
className={buttonBarClass(
sequenceEntryStatus.status !== hasErrorsStatus &&
sequenceEntryStatus.status !== awaitingApprovalStatus,
)}
data-testid={`${getAccessionVersionString({ ...sequenceEntryStatus })}.edit`}
data-tooltip-id={'edit-tooltip' + sequenceEntryStatus.accession}
onClick={editAccessionVersion}
disabled={
sequenceEntryStatus.status !== hasErrorsStatus &&
sequenceEntryStatus.status !== awaitingApprovalStatus
}
>
<ClarityNoteEditLine />
</button>
)}
<Tooltip
id={'edit-tooltip' + sequenceEntryStatus.accession}
content={
Expand All @@ -151,13 +168,15 @@ const ButtonBar: FC<ButtonBarProps> = ({
<button
className={buttonBarClass(
sequenceEntryStatus.status !== hasErrorsStatus &&
sequenceEntryStatus.status !== awaitingApprovalStatus,
sequenceEntryStatus.status !== awaitingApprovalStatus &&
sequenceEntryStatus.status !== awaitingApprovalForRevocationStatus,
)}
onClick={deleteAccessionVersion}
data-tooltip-id={'delete-tooltip' + sequenceEntryStatus.accession}
disabled={
sequenceEntryStatus.status !== hasErrorsStatus &&
sequenceEntryStatus.status !== awaitingApprovalStatus
sequenceEntryStatus.status !== awaitingApprovalStatus &&
sequenceEntryStatus.status !== awaitingApprovalForRevocationStatus
}
>
<BiTrash />
Expand All @@ -166,7 +185,8 @@ const ButtonBar: FC<ButtonBarProps> = ({
id={'delete-tooltip' + sequenceEntryStatus.accession}
content={
sequenceEntryStatus.status !== hasErrorsStatus &&
sequenceEntryStatus.status !== awaitingApprovalStatus
sequenceEntryStatus.status !== awaitingApprovalStatus &&
sequenceEntryStatus.status !== awaitingApprovalForRevocationStatus
? 'Cannot discard. Wait for preprocessing.'
: 'Discard this sequence entry'
}
Expand Down
6 changes: 3 additions & 3 deletions website/src/components/ReviewPage/ReviewPage.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,13 @@ describe('ReviewPage', () => {
getByText('Discard sequences').click();

await waitFor(() => {
expect(getByText((text) => text.includes('Discard 1 sequences with errors'))).toBeDefined();
expect(getByText((text) => text.includes('Release 1 sequences without errors'))).toBeDefined();
expect(getByText((text) => text.includes('Discard 1 sequence with errors'))).toBeDefined();
expect(getByText((text) => text.includes('Release 1 valid sequence'))).toBeDefined();
});

mockRequest.backend.getSequences(200, generateGetSequencesResponse([]));

getByText((text) => text.includes('Release 1 sequences without errors')).click();
getByText((text) => text.includes('Release 1 valid sequence')).click();
await waitFor(() => {
expect(getByText('Confirm')).toBeDefined();
});
Expand Down
22 changes: 14 additions & 8 deletions website/src/components/ReviewPage/ReviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
deleteProcessedDataWithErrorsScope,
hasErrorsStatus,
inProcessingStatus,
awaitingApprovalForRevocationStatus,
type PageQuery,
type SequenceEntryStatus,
} from '../../types/backend.ts';
Expand Down Expand Up @@ -64,12 +65,16 @@ const InnerReviewPage: FC<ReviewPageProps> = ({ clientConfig, organism, accessTo
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 finishedCount = processedCount + errorCount + revocationCount;

const sequences: SequenceEntryStatus[] = hooks.getSequences.data.sequenceEntries;

const controlPanel = (
<div className='flex flex-col py-2'>
<div>
{processedCount + errorCount} of {total} sequences processed.
{finishedCount} of {total} sequences processed.
{processingCount > 0 && <span className='loading loading-spinner loading-sm ml-3'> </span>}
</div>
<div>
Expand Down Expand Up @@ -112,7 +117,7 @@ const InnerReviewPage: FC<ReviewPageProps> = ({ clientConfig, organism, accessTo

const bulkActionButtons = (
<div className='flex justify-end items-center gap-3'>
{processedCount + errorCount > 0 && (
{finishedCount > 0 && (
<Menu as='div' className='relative inline-block text-left'>
<Menu.Button className='border rounded-md p-1 bg-gray-500 text-white px-2'>
<BiTrash className='inline-block w-4 h-4 -mt-0.5 mr-1.5' />
Expand All @@ -138,7 +143,7 @@ const InnerReviewPage: FC<ReviewPageProps> = ({ clientConfig, organism, accessTo
}
>
<BiTrash className='inline-block w-4 h-4 -mt-0.5 mr-1.5' />
Discard {errorCount} sequences with errors
Discard {errorCount} sequence{errorCount > 1 ? 's' : ''} with errors
</button>
</Menu.Item>
)}
Expand All @@ -147,7 +152,7 @@ const InnerReviewPage: FC<ReviewPageProps> = ({ clientConfig, organism, accessTo
className={menuItemClassName}
onClick={() =>
displayConfirmationDialog({
dialogText: `Are you sure you want to discard all ${processedCount + errorCount} processed sequences?`,
dialogText: `Are you sure you want to discard all ${finishedCount} processed sequences?`,
onConfirmation: () => {
hooks.deleteSequenceEntries({
scope: deleteAllDataScope.value,
Expand All @@ -157,19 +162,19 @@ const InnerReviewPage: FC<ReviewPageProps> = ({ clientConfig, organism, accessTo
}
>
<BiTrash className='inline-block w-4 h-4 -mt-0.5 mr-1.5' />
Discard all {processedCount + errorCount} processed sequences
Discard all {finishedCount} processed sequences
</button>
</Menu.Item>
</div>
</Menu.Items>
</Menu>
)}
{processedCount > 0 && (
{processedCount + revocationCount > 0 && (
<button
className='border rounded-md p-1 bg-gray-500 text-white px-2'
onClick={() =>
displayConfirmationDialog({
dialogText: 'Are you sure you want to release all sequences without errors?',
dialogText: 'Are you sure you want to release all valid sequences?',
onConfirmation: () =>
hooks.approveProcessedData({
scope: approveAllDataScope.value,
Expand All @@ -178,7 +183,8 @@ const InnerReviewPage: FC<ReviewPageProps> = ({ clientConfig, organism, accessTo
}
>
<WpfPaperPlane className='inline-block w-4 h-4 -mt-0.5 mr-1.5' />
Release {processedCount} sequences without errors
Release {processedCount + revocationCount} valid sequence
{processedCount + revocationCount > 1 ? 's' : ''}
</button>
)}
</div>
Expand Down
49 changes: 49 additions & 0 deletions website/src/components/SequenceDetailsPage/RevokeButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { type FC } from 'react';
import { toast } from 'react-toastify';

import { routes } from '../../routes';
import { backendClientHooks } from '../../services/serviceHooks';
import type { ClientConfig } from '../../types/runtimeConfig';
import { createAuthorizationHeader } from '../../utils/createAuthorizationHeader';
import { stringifyMaybeAxiosError } from '../../utils/stringifyMaybeAxiosError';
import { withQueryProvider } from '../common/withQueryProvider';

type RevokeSequenceEntryProps = {
organism: string;
accessToken: string;
clientConfig: ClientConfig;
accessionVersion: string;
};

const InnerRevokeButton: FC<RevokeSequenceEntryProps> = ({ organism, accessToken, clientConfig, accessionVersion }) => {
const hooks = backendClientHooks(clientConfig);
const useRevokeSequenceEntries = hooks.useRevokeSequences(
{ headers: createAuthorizationHeader(accessToken), params: { organism } },
{
onSuccess: () => {
document.location = routes.userSequenceReviewPage(organism);
},
onError: (error) =>
toast.error(getRevokeSequenceEntryErrorMessage(error), {
position: 'top-center',
autoClose: false,
}),
},
);

const handleRevokeSequenceEntry = () => {
useRevokeSequenceEntries.mutate({ accessions: [accessionVersion] });
};

return (
<button className='btn btn-sm bg-red-400' onClick={handleRevokeSequenceEntry}>
Revoke this sequence
</button>
);
};

export const RevokeButton = withQueryProvider(InnerRevokeButton);

function getRevokeSequenceEntryErrorMessage(error: unknown) {
return 'Failed to revoke sequence entry: ' + stringifyMaybeAxiosError(error);
}
Loading
Loading