From 8373c73a4fa7e31c7b3fb2c14e36f37324044f8f Mon Sep 17 00:00:00 2001 From: Fabian Engelniederhammer <92720311+fengelniederhammer@users.noreply.github.com> Date: Fri, 20 Dec 2024 08:50:31 +0100 Subject: [PATCH] fix(website): show error when parsing the collection variant filter fails (#438) * refactor(website): extract hook * fix(website): show error when parsing the collection variant filter fails preparation for #436 --- .../src/components/ErrorReportInstruction.tsx | 44 ++++++++++++-- .../create/SubscriptionsCreate.tsx | 23 +++----- .../overview/SubscriptionEntry.tsx | 26 +++------ .../analyzeSingleVariant/CollectionsList.tsx | 57 +++++++++++++------ website/src/styles/containers/Modal.tsx | 15 +++-- website/src/styles/containers/ModalBox.tsx | 4 +- 6 files changed, 110 insertions(+), 59 deletions(-) diff --git a/website/src/components/ErrorReportInstruction.tsx b/website/src/components/ErrorReportInstruction.tsx index 9d3a5acb..ade7b059 100644 --- a/website/src/components/ErrorReportInstruction.tsx +++ b/website/src/components/ErrorReportInstruction.tsx @@ -1,4 +1,40 @@ import { useRef, useState } from 'react'; +import { toast } from 'react-toastify'; +import { v4 as uuidv4 } from 'uuid'; + +import { Modal, useModalRef } from '../styles/containers/Modal.tsx'; +import type { InstanceLogger } from '../types/logMessage.ts'; + +type ErrorToastArguments = { + error: Error; + logMessage: string; + errorToastMessages: [string, ...string[]]; +}; + +export function useErrorToast(logger: InstanceLogger) { + return { + showErrorToast: ({ error, logMessage, errorToastMessages }: ErrorToastArguments) => { + const errorId = uuidv4(); + logger.error(logMessage, { + errorId, + }); + toast.error( + <> + {errorToastMessages.map((toastMessage, index) => ( +

+ {toastMessage} +

+ ))} + + , + { + position: 'bottom-left', + autoClose: false, + }, + ); + }, + }; +} /** * Throw this error if you want to display the error message to the user. @@ -6,7 +42,7 @@ import { useRef, useState } from 'react'; export class UserFacingError extends Error {} export function ErrorReportToastModal({ errorId, error }: { errorId: string; error: Error }) { - const modalRef = useRef(null); + const modalRef = useModalRef(); const openModal = () => { if (modalRef.current) { @@ -23,8 +59,8 @@ export function ErrorReportToastModal({ errorId, error }: { errorId: string; err Help us fix the issue.

- -
+ +
@@ -32,7 +68,7 @@ export function ErrorReportToastModal({ errorId, error }: { errorId: string; err
-
+ ); } diff --git a/website/src/components/subscriptions/create/SubscriptionsCreate.tsx b/website/src/components/subscriptions/create/SubscriptionsCreate.tsx index 3c8e6c90..4a9acc04 100644 --- a/website/src/components/subscriptions/create/SubscriptionsCreate.tsx +++ b/website/src/components/subscriptions/create/SubscriptionsCreate.tsx @@ -2,8 +2,6 @@ import '@genspectrum/dashboard-components/components'; import '@genspectrum/dashboard-components/style.css'; import { useMutation } from '@tanstack/react-query'; import { useState } from 'react'; -import { toast } from 'react-toastify'; -import { v4 as uuidv4 } from 'uuid'; import { FilterDisplay } from './FilterDisplay.tsx'; import { IntervalInput } from './IntervalInput.tsx'; @@ -22,7 +20,7 @@ import { type EvaluationInterval, EvaluationIntervals } from '../../../types/Eva import { type Organism, Organisms } from '../../../types/Organism.ts'; import type { SubscriptionRequest, Trigger } from '../../../types/Subscription.ts'; import { getErrorLogMessage } from '../../../util/getErrorLogMessage.ts'; -import { ErrorReportToastModal } from '../../ErrorReportInstruction.tsx'; +import { useErrorToast } from '../../ErrorReportInstruction.tsx'; import { GsApp } from '../../genspectrum/GsApp.tsx'; import { getBackendServiceForClientside } from '../backendApi/backendService.ts'; import { withQueryProvider } from '../backendApi/withQueryProvider.tsx'; @@ -42,6 +40,8 @@ export function SubscriptionsCreateInner({ // TODO: Enable notificationChannels in #82, #128 // notificationChannels: NotificationChannels; }) { + const { showErrorToast } = useErrorToast(logger); + const createSubscription = useMutation({ mutationFn: () => getBackendServiceForClientside().postSubscription({ @@ -49,20 +49,11 @@ export function SubscriptionsCreateInner({ userId, }), onError: (error) => { - const errorId = uuidv4(); - logger.error(`Failed to create a new subscription: ${getErrorLogMessage(error)}`, { - errorId, + showErrorToast({ + error, + logMessage: `Failed to create a new subscription: ${getErrorLogMessage(error)}`, + errorToastMessages: ['We could not create your subscription. Please try again later.'], }); - toast.error( - <> -

We could not create your subscription. Please try again later.

- - , - { - position: 'bottom-left', - autoClose: false, - }, - ); }, }); diff --git a/website/src/components/subscriptions/overview/SubscriptionEntry.tsx b/website/src/components/subscriptions/overview/SubscriptionEntry.tsx index 625e9933..82f330b9 100644 --- a/website/src/components/subscriptions/overview/SubscriptionEntry.tsx +++ b/website/src/components/subscriptions/overview/SubscriptionEntry.tsx @@ -1,7 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import { type JSX, type RefObject } from 'react'; import { toast } from 'react-toastify'; -import { v4 as uuidv4 } from 'uuid'; import { SubscriptionDisplay } from './SubscriptionDisplay.tsx'; import { getClientLogger } from '../../../clientLogger.ts'; @@ -13,7 +12,7 @@ import { ModalHeader } from '../../../styles/containers/ModalHeader.tsx'; import { organismConfig } from '../../../types/Organism.ts'; import type { Subscription } from '../../../types/Subscription.ts'; import { getErrorLogMessage } from '../../../util/getErrorLogMessage.ts'; -import { ErrorReportToastModal } from '../../ErrorReportInstruction.tsx'; +import { useErrorToast } from '../../ErrorReportInstruction.tsx'; import { getBackendServiceForClientside } from '../backendApi/backendService.ts'; const logger = getClientLogger('SubscriptionEntry'); @@ -102,6 +101,8 @@ function MoreDropdown({ userId: string; refetchSubscriptions: () => void; }) { + const { showErrorToast } = useErrorToast(logger); + const deleteSubscription = useMutation({ mutationFn: () => getBackendServiceForClientside().deleteSubscription({ @@ -116,22 +117,13 @@ function MoreDropdown({ }); }, onError: (error) => { - const errorId = uuidv4(); - logger.error(`Failed to delete subscription with id '${subscription.id}': ${getErrorLogMessage(error)}`, { - errorId, + showErrorToast({ + error, + logMessage: `Failed to delete subscription with id '${subscription.id}': ${getErrorLogMessage(error)}`, + errorToastMessages: [ + `We could not delete your subscription "${subscription.name}". Please try again later.`, + ], }); - toast.error( - <> -

- We could not delete your subscription "{subscription.name}". Please try again later. -

- - , - { - position: 'bottom-left', - autoClose: false, - }, - ); }, }); diff --git a/website/src/components/views/analyzeSingleVariant/CollectionsList.tsx b/website/src/components/views/analyzeSingleVariant/CollectionsList.tsx index 8c6048c6..20b67ff3 100644 --- a/website/src/components/views/analyzeSingleVariant/CollectionsList.tsx +++ b/website/src/components/views/analyzeSingleVariant/CollectionsList.tsx @@ -2,9 +2,11 @@ import { useQuery } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import { z } from 'zod'; +import { getClientLogger } from '../../../clientLogger.ts'; import type { OrganismsConfig } from '../../../config.ts'; import { type CovidVariantData } from '../../../views/covid.ts'; import { Routing } from '../../../views/routing.ts'; +import { useErrorToast } from '../../ErrorReportInstruction.tsx'; import { withQueryProvider } from '../../subscriptions/backendApi/withQueryProvider.tsx'; type CollectionVariant = { @@ -98,14 +100,51 @@ const querySchema = z.object({ function CollectionVariantList({ collection, organismsConfig }: CollectionVariantListProps) { const variants = collection.variants; + const selectVariant = useSelectVariant(organismsConfig, collection); + + return ( +
+ {variants.map((variant, index) => ( + + ))} +
+ ); +} + +const logger = getClientLogger('CollectionList'); + +function useSelectVariant(organismsConfig: OrganismsConfig, collection: Collection) { const routing = useMemo(() => new Routing(organismsConfig), [organismsConfig]); - const selectVariant = (variant: CollectionVariant) => { + const { showErrorToast } = useErrorToast(logger); + + return (variant: CollectionVariant) => { const currentPageState = routing .getOrganismView('covid.singleVariantView') .pageStateHandler.parsePageStateFromUrl(new URL(window.location.href)); let newPageState: CovidVariantData; - const query = querySchema.parse(JSON.parse(variant.query)); + + const queryParseResult = querySchema.safeParse(JSON.parse(variant.query)); + + if (!queryParseResult.success) { + showErrorToast({ + error: queryParseResult.error, + logMessage: `Failed to parse query of variant ${variant.name} of collection ${collection.id}: ${queryParseResult.error.message}`, + errorToastMessages: [ + `The variant filter of the collection variant "${variant.name}" seems to be invalid.`, + ], + }); + return; + } + + const query = queryParseResult.data; + if (query.variantQuery !== undefined) { newPageState = { ...currentPageState, @@ -135,18 +174,4 @@ function CollectionVariantList({ collection, organismsConfig }: CollectionVarian } window.location.href = routing.getOrganismView('covid.singleVariantView').pageStateHandler.toUrl(newPageState); }; - - return ( -
- {variants.map((variant, index) => ( - - ))} -
- ); } diff --git a/website/src/styles/containers/Modal.tsx b/website/src/styles/containers/Modal.tsx index 6b9b4c1d..e7b7cd30 100644 --- a/website/src/styles/containers/Modal.tsx +++ b/website/src/styles/containers/Modal.tsx @@ -6,7 +6,11 @@ export function useModalRef() { return useRef(null); } -type ModalProps = +const modalSize = { + large: 'max-w-screen-lg', +}; + +type ModalProps = ( | { /** set this when using from React and open it via `modalRef.current?.showModal()` */ modalRef: RefObject; @@ -16,12 +20,15 @@ type ModalProps = /** set this when using from Astro and open it via `onclick="id.showModal()"` */ id: string; modalRef?: undefined; - }; + } +) & { + size?: keyof typeof modalSize; +}; -export const Modal: FC> = ({ children, modalRef, id }) => { +export const Modal: FC> = ({ children, modalRef, id, size }) => { return ( - {children} + {children}
diff --git a/website/src/styles/containers/ModalBox.tsx b/website/src/styles/containers/ModalBox.tsx index edd5e9c4..bd877e9f 100644 --- a/website/src/styles/containers/ModalBox.tsx +++ b/website/src/styles/containers/ModalBox.tsx @@ -1,5 +1,5 @@ import { type PropsWithChildren } from 'react'; -export function ModalBox({ children }: PropsWithChildren) { - return
{children}
; +export function ModalBox({ children, className }: PropsWithChildren<{ className?: string }>) { + return
{children}
; }