Skip to content

Commit

Permalink
feat(components): also display error details when LAPIS calls fail
Browse files Browse the repository at this point in the history
closes #366
  • Loading branch information
fengelniederhammer committed Aug 2, 2024
1 parent 9b9f0c2 commit 56bd18f
Show file tree
Hide file tree
Showing 19 changed files with 92 additions and 67 deletions.
4 changes: 4 additions & 0 deletions components/.storybook-preact/preview.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Preview } from '@storybook/preact';

import '../src/styles/tailwind.css';
import { withActions } from '@storybook/addon-actions/decorator';
import { GS_ERROR_EVENT_TYPE } from '../src/preact/components/error-display';

const preview: Preview = {
parameters: {
Expand All @@ -10,7 +12,9 @@ const preview: Preview = {
date: /Date$/i,
},
},
actions: { handles: [GS_ERROR_EVENT_TYPE] },
},
decorators: [withActions],
};

export default preview;
6 changes: 6 additions & 0 deletions components/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { REFERENCE_GENOME_ENDPOINT } from '../src/constants';
import referenceGenome from '../src/lapisApi/__mockData__/referenceGenome.json';
import customElements from '../custom-elements.json';
import DocumentationTemplate from './DocumentationTemplate.mdx';
import { withActions } from '@storybook/addon-actions/decorator';
import { GS_ERROR_EVENT_TYPE } from '../src/preact/components/error-display';

setCustomElementsManifest(customElements);

Expand Down Expand Up @@ -35,7 +37,11 @@ const preview: Preview = {
docs: {
page: DocumentationTemplate,
},
actions: { handles: [GS_ERROR_EVENT_TYPE] },
},
decorators: [withActions],
};

export default preview;

export const previewHandles = preview.parameters!.actions.handles;
22 changes: 15 additions & 7 deletions components/src/lapisApi/lapisApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class UnknownLapisError extends Error {
constructor(
message: string,
public readonly status: number,
public readonly requestedData: string,
) {
super(message);
this.name = 'UnknownLapisError';
Expand All @@ -26,6 +27,7 @@ export class LapisError extends Error {
message: string,
public readonly status: number,
public readonly problemDetail: ProblemDetail,
public readonly requestedData: string,
) {
super(message);
this.name = 'LapisError';
Expand All @@ -42,7 +44,7 @@ export async function fetchAggregated(lapisUrl: string, body: LapisBaseRequest,
signal,
});

await handleErrors(response);
await handleErrors(response, 'aggregated data');

return aggregatedResponse.parse(await response.json());
}
Expand All @@ -62,7 +64,7 @@ export async function fetchInsertions(
signal,
});

await handleErrors(response);
await handleErrors(response, `${sequenceType} insertions`);

return insertionsResponse.parse(await response.json());
}
Expand All @@ -82,7 +84,7 @@ export async function fetchSubstitutionsOrDeletions(
signal,
});

await handleErrors(response);
await handleErrors(response, `${sequenceType} mutations`);

return mutationsResponse.parse(await response.json());
}
Expand All @@ -96,11 +98,11 @@ export async function fetchReferenceGenome(lapisUrl: string, signal?: AbortSigna
signal,
});

await handleErrors(response);
await handleErrors(response, 'the reference genomes');
return referenceGenomeResponse.parse(await response.json());
}

const handleErrors = async (response: Response) => {
const handleErrors = async (response: Response, requestedData: string) => {
if (!response.ok) {
if (response.status >= 400 && response.status < 500) {
const json = await response.json();
Expand All @@ -111,6 +113,7 @@ const handleErrors = async (response: Response) => {
response.statusText + lapisErrorResult.data.error.detail,
response.status,
lapisErrorResult.data.error,
requestedData,
);
}

Expand All @@ -120,12 +123,17 @@ const handleErrors = async (response: Response) => {
response.statusText + problemDetailResult.data.detail,
response.status,
problemDetailResult.data,
requestedData,
);
}

throw new UnknownLapisError(`${response.statusText}: ${JSON.stringify(json)}`, response.status);
throw new UnknownLapisError(
`${response.statusText}: ${JSON.stringify(json)}`,
response.status,
requestedData,
);
}
throw new UnknownLapisError(`${response.statusText}: ${response.status}`, response.status);
throw new UnknownLapisError(`${response.statusText}: ${response.status}`, response.status, requestedData);
}
};

Expand Down
5 changes: 1 addition & 4 deletions components/src/preact/components/error-boundary.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { withActions } from '@storybook/addon-actions/decorator';
import { type Meta, type StoryObj } from '@storybook/preact';
import { expect, waitFor, within } from '@storybook/test';

import { ErrorBoundary } from './error-boundary';
import { GS_ERROR_EVENT_TYPE, UserFacingError } from './error-display';
import { UserFacingError } from './error-display';

const meta: Meta = {
title: 'Component/Error boundary',
component: ErrorBoundary,
parameters: {
fetchMock: {},
actions: { handles: [GS_ERROR_EVENT_TYPE] },
},
argTypes: {
size: { control: 'object' },
Expand All @@ -19,7 +17,6 @@ const meta: Meta = {
args: {
size: { height: '600px', width: '100%' },
},
decorators: [withActions],
};

export default meta;
Expand Down
51 changes: 43 additions & 8 deletions components/src/preact/components/error-display.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { type FunctionComponent } from 'preact';
import { useEffect, useRef } from 'preact/hooks';

import { LapisError, UnknownLapisError } from '../../lapisApi/lapisApi';

export const GS_ERROR_EVENT_TYPE = 'gs-error';

export interface ErrorEvent extends Event {
Expand Down Expand Up @@ -33,21 +35,20 @@ export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) =>
);
});

const { headline, details } = getDisplayData(error);

return (
<div
ref={containerRef}
className='h-full w-full rounded-md border-2 border-gray-100 p-2 flex items-center justify-center flex-col'
>
<div className='text-red-700 font-bold'>Error</div>
<div className='text-red-700 font-bold'>{headline}</div>
<div>
Oops! Something went wrong.
{error instanceof UserFacingError && (
{details !== undefined && (
<>
{' '}
<button
className='text-sm text-gray-600 hover:text-gray-300'
onClick={() => ref.current?.showModal()}
>
<button className='underline hover:text-gray-400' onClick={() => ref.current?.showModal()}>
Show details.
</button>
<dialog ref={ref} class='modal'>
Expand All @@ -57,8 +58,8 @@ export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) =>
</button>
</form>
<h1 class='text-lg'>{error.headline}</h1>
<p class='py-4'>{error.message}</p>
<h1 class='text-lg'>{details.headline}</h1>
<p class='py-4'>{details.message}</p>
</div>
<form method='dialog' class='modal-backdrop'>
<button>close</button>
Expand All @@ -70,3 +71,37 @@ export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) =>
</div>
);
};

function getDisplayData(error: Error) {
if (error instanceof UserFacingError) {
return {
headline: `Error - ${error.headline}`,
details: {
headline: error.headline,
message: error.message,
},
};
}

if (error instanceof LapisError) {
return {
headline: `Error - Failed fetching ${error.requestedData} from LAPIS`,
details: {
headline: `LAPIS request failed: ${error.requestedData} - ${error.problemDetail.status} ${error.problemDetail.title}`,
message: error.problemDetail.detail ?? error.message,
},
};
}

if (error instanceof UnknownLapisError) {
return {
headline: `Error - Failed fetching ${error.requestedData} from LAPIS`,
details: {
headline: `LAPIS request failed: An unexpected error occurred while fetching ${error.requestedData}`,
message: error.message,
},
};
}

return { headline: 'Error', details: undefined };
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { withActions } from '@storybook/addon-actions/decorator';
import { type Meta, type StoryObj } from '@storybook/preact';
import { expect, waitFor, within } from '@storybook/test';
import dayjs from 'dayjs/esm';
Expand All @@ -13,6 +12,7 @@ import {
PRESET_VALUE_LAST_6_MONTHS,
PRESET_VALUE_LAST_MONTH,
} from './selectableOptions';
import { previewHandles } from '../../../.storybook/preview';
import { LAPIS_URL } from '../../constants';
import { LapisUrlContext } from '../LapisUrlContext';

Expand All @@ -23,7 +23,7 @@ const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
component: DateRangeSelector,
parameters: {
actions: {
handles: ['gs-date-range-changed'],
handles: ['gs-date-range-changed', previewHandles],
},
fetchMock: {},
},
Expand Down Expand Up @@ -68,7 +68,6 @@ const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
initialDateFrom: '',
initialDateTo: '',
},
decorators: [withActions],
};

export default meta;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { withActions } from '@storybook/addon-actions/decorator';
import { type Meta, type StoryObj } from '@storybook/preact';

import { LineageFilter, type LineageFilterProps } from './lineage-filter';
import { previewHandles } from '../../../.storybook/preview';
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
import aggregatedData from '../../preact/lineageFilter/__mockData__/aggregated.json';
import { LapisUrlContext } from '../LapisUrlContext';
Expand All @@ -11,7 +11,7 @@ const meta: Meta = {
component: LineageFilter,
parameters: {
actions: {
handles: ['gs-lineage-filter-changed'],
handles: ['gs-lineage-filter-changed', previewHandles],
},
fetchMock: {
mocks: [
Expand All @@ -31,7 +31,6 @@ const meta: Meta = {
],
},
},
decorators: [withActions],
};

export default meta;
Expand Down
15 changes: 1 addition & 14 deletions components/src/preact/locationFilter/fetchAutocompletionList.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { LapisError } from '../../lapisApi/lapisApi';
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
import { UserFacingError } from '../components/error-display';

export async function fetchAutocompletionList(fields: string[], lapis: string, signal?: AbortSignal) {
const toAncestorInHierarchyOverwriteValues = Array(fields.length - 1)
Expand All @@ -10,18 +8,7 @@ export async function fetchAutocompletionList(fields: string[], lapis: string, s

const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string | null>>({}, fields);

let data;
try {
data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
} catch (error) {
if (error instanceof LapisError) {
throw new UserFacingError(
`Failed to fetch autocomplete list from LAPIS: ${error.problemDetail.status} ${error.problemDetail.title ?? ''}`,
error.problemDetail.detail ?? error.message,
);
}
throw error;
}
const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;

const locationValues = data
.map((entry) => fields.reduce((acc, field) => ({ ...acc, [field]: entry[field] }), {}))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { withActions } from '@storybook/addon-actions/decorator';
import { type Meta, type StoryObj } from '@storybook/preact';

import data from './__mockData__/aggregated.json';
import { LocationFilter, type LocationFilterProps } from './location-filter';
import { previewHandles } from '../../../.storybook/preview';
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
import { LapisUrlContext } from '../LapisUrlContext';

Expand All @@ -28,7 +28,7 @@ const meta: Meta<LocationFilterProps> = {
],
},
actions: {
handles: ['gs-location-changed'],
handles: ['gs-location-changed', previewHandles],
},
},
args: {
Expand Down Expand Up @@ -59,7 +59,6 @@ const meta: Meta<LocationFilterProps> = {
},
},
},
decorators: [withActions],
};

export default meta;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { withActions } from '@storybook/addon-actions/decorator';
import { type Meta, type PreactRenderer, type StoryObj } from '@storybook/preact';
import { expect, fireEvent, fn, userEvent, waitFor, within } from '@storybook/test';
import { type StepFunction } from '@storybook/types';

import { MutationFilter, type MutationFilterProps } from './mutation-filter';
import { previewHandles } from '../../../.storybook/preview';
import { LAPIS_URL } from '../../constants';
import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
import { LapisUrlContext } from '../LapisUrlContext';
Expand All @@ -14,7 +14,7 @@ const meta: Meta<MutationFilterProps> = {
component: MutationFilter,
parameters: {
actions: {
handles: ['gs-mutation-filter-changed', 'gs-mutation-filter-on-blur'],
handles: ['gs-mutation-filter-changed', 'gs-mutation-filter-on-blur', previewHandles],
},
fetchMock: {},
},
Expand All @@ -26,7 +26,6 @@ const meta: Meta<MutationFilterProps> = {
},
},
},
decorators: [withActions],
};

export default meta;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const PrevalenceOverTimeInner: FunctionComponent<PrevalenceOverTimeProps>
lapis,
lapisDateField,
),
[lapis, numeratorFilter, denominatorFilter, granularity, smoothingWindow],
[lapis, numeratorFilter, denominatorFilter, granularity, smoothingWindow, lapisDateField],
);

if (isLoading) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const RelativeGrowthAdvantageInner: FunctionComponent<RelativeGrowthAdvan

const { data, error, isLoading } = useQuery(
() => queryRelativeGrowthAdvantage(numeratorFilter, denominatorFilter, generationTime, lapis, lapisDateField),
[lapis, numeratorFilter, denominatorFilter, generationTime, views],
[lapis, numeratorFilter, denominatorFilter, generationTime, views, lapisDateField],
);

if (isLoading) {
Expand Down
Loading

0 comments on commit 56bd18f

Please sign in to comment.