Skip to content

Commit

Permalink
implement initial error reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
jthrilly committed Nov 8, 2023
1 parent 6660e24 commit ff91a03
Show file tree
Hide file tree
Showing 14 changed files with 345 additions and 154 deletions.
196 changes: 132 additions & 64 deletions app/(dashboard)/dashboard/_components/ProtocolUploader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,29 @@
import { useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { Button } from '~/components/ui/Button';
import { Collapsible, CollapsibleContent } from '~/components/ui/collapsible';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '~/components/ui/collapsible';
import ActiveProtocolSwitch from '~/app/(dashboard)/dashboard/_components/ActiveProtocolSwitch';
import {
getAssets,
getProtocolAssets,
getProtocolJson,
readFileHelper,
fileAsArrayBuffer,
} from '~/utils/protocolImport';
import ErrorDialog from '~/components/ui/ErrorDialog';
import { useToast } from '~/components/ui/use-toast';
import { Progress } from '~/components/ui/progress';
import { api } from '~/trpc/client';
import { uploadFiles } from '~/lib/uploadthing-helpers';
import { clientRevalidateTag } from '~/utils/clientRevalidate';
import { useRouter } from 'next/navigation';
import { DatabaseError } from '~/utils/databaseError';
import { ensureError } from '~/utils/ensureError';
import { ValidationError } from '@codaco/protocol-validation';
import Link from 'next/link';
import { ErrorDetails } from '~/components/ErrorDetails';

type ErrorState = {
title: string;
Expand All @@ -28,6 +38,7 @@ type ProgressState = {
};

export default function ProtocolUploader() {
const router = useRouter();
const [error, setError] = useState<ErrorState | null>(null);
const [progress, setProgress] = useState<ProgressState | null>(null);
const { toast } = useToast();
Expand All @@ -49,19 +60,10 @@ export default function ProtocolUploader() {
percent: 0,
status: 'Reading file...',
});
const content = await readFileHelper(acceptedFile);

if (!content) {
setError({
title: 'Error reading file',
description: 'The file could not be read',
});
setProgress(null);
return;
}

const JSZip = (await import('jszip')).default;
const zip = await JSZip.loadAsync(content);
const fileArrayBuffer = await fileAsArrayBuffer(acceptedFile);
const JSZip = (await import('jszip')).default; // Dynamic import to reduce bundle size
const zip = await JSZip.loadAsync(fileArrayBuffer);
const protocolJson = await getProtocolJson(zip);

// Validating protocol...
Expand All @@ -70,44 +72,17 @@ export default function ProtocolUploader() {
status: 'Validating protocol...',
});

const { validateProtocol, ValidationError } = await import(
const { validateProtocol } = await import(
'@codaco/protocol-validation'
);

try {
await validateProtocol(protocolJson);
} catch (error) {
if (error instanceof ValidationError) {
setError({
title: 'Protocol was invalid!',
description: 'The protocol you uploaded was invalid.',
additionalContent: (
<Collapsible>
<CollapsibleContent>
<div>
<p>Errors:</p>
<ul>
{error.logicErrors.map((e, i) => (
<li key={i}>{e}</li>
))}
{error.schemaErrors.map((e, i) => (
<li key={i}>{e}</li>
))}
</ul>
</div>
</CollapsibleContent>
</Collapsible>
),
});
setProgress(null);
return;
}
// This function will throw on validation errors, with type ValidationError
await validateProtocol(protocolJson);

throw error;
}
// After this point, assume the protocol is valid.
const assets = await getProtocolAssets(protocolJson, zip);

// Protocol is valid, continue with import
const assets = await getAssets(protocolJson, zip);
console.log('assets', assets);

setProgress({
percent: 0,
Expand All @@ -119,8 +94,8 @@ export default function ProtocolUploader() {
const completeCount = assets.length * 100;
let currentProgress = 0;

const response = await uploadFiles({
files: assets,
const uploadedFiles = await uploadFiles({
files: assets.map((asset) => asset.file),
endpoint: 'assetRouter',
onUploadProgress({ progress }) {
currentProgress += progress;
Expand All @@ -131,21 +106,43 @@ export default function ProtocolUploader() {
},
});

console.log('asset upload response', response);
// The asset 'name' prop matches across the assets array and the
// uploadedFiles array, so we can just map over one of them and
// merge the properties we need to add to the database.
const assetsWithUploadMeta = assets.map((asset) => {
const uploadedAsset = uploadedFiles.find(
(uploadedFile) => uploadedFile.name === asset.name,
);

if (!uploadedAsset) {
throw new Error('Asset upload failed');
}

return {
key: uploadedAsset.key,
assetId: asset.assetId,
name: asset.name,
type: asset.type,
url: uploadedAsset.url,
size: uploadedAsset.size,
};
});

setProgress({
percent: 100,
status: 'Creating database entry for protocol...',
});

await insertProtocol({
const result = await insertProtocol({
protocol: protocolJson,
protocolName: fileName,
assets: response.map((fileResponse) => ({
assetId: fileResponse.key,
key: fileResponse.key,
source: fileResponse.key,
url: fileResponse.url,
name: fileResponse.name,
size: fileResponse.size,
})),
assets: assetsWithUploadMeta,
});

if (result.error) {
throw new DatabaseError(result.error, result.errorDetails);
}

toast({
title: 'Protocol imported!',
description: 'Your protocol has been successfully imported.',
Expand All @@ -154,12 +151,83 @@ export default function ProtocolUploader() {

setProgress(null);
await clientRevalidateTag('protocol.get.all');
router.refresh();
} catch (e) {
// eslint-disable-next-line no-console
console.log(e);
setError({
title: 'Error importing protocol',
description: e.message,
});

const error = ensureError(e);

// Validation errors come from @codaco/protocol-validation
if (error instanceof ValidationError) {
setError({
title: 'Protocol was invalid!',
description: (
<>
<p>
The protocol you uploaded was invalid. Please see the details
below for specific validation errors that were found.
</p>
<p>
If you believe that your protocol should be valid please ask
for help via our{' '}
<Link
href="https://community.networkcanvas.com"
target="_blank"
>
community forum
</Link>
.
</p>
</>
),
additionalContent: (
<ErrorDetails>
<>
<p>{error.message}</p>
<p>Errors:</p>
<ul>
{error.logicErrors.map((e, i) => (
<li key={i}>{e}</li>
))}
{error.schemaErrors.map((e, i) => (
<li key={i}>{e}</li>
))}
</ul>
</>
</ErrorDetails>
),
});
}
// Database errors are thrown inside our tRPC router
else if (error instanceof DatabaseError) {
setError({
title: 'Database error during protocol import',
description: error.message,
additionalContent: (
<ErrorDetails>
<pre>{error.originalError.toString()}</pre>
</ErrorDetails>
),
});
} else {
setError({
title: 'Error importing protocol',
description:
'There was an unknown error while importing your protocol. The information below might help us to debug the issue.',
additionalContent: (
<ErrorDetails>
<pre>
<strong>Message: </strong>
{error.message}

<strong>Stack: </strong>
{error.stack}
</pre>
</ErrorDetails>
),
});
}
setProgress(null);
}
},
Expand Down
2 changes: 2 additions & 0 deletions app/(dashboard)/dashboard/protocols/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import ProtocolUploader from '../_components/ProtocolUploader';
import { ProtocolsTable } from '../_components/ProtocolsTable/ProtocolsTable';
import { api } from '~/trpc/server';

export const dynamic = 'force-dynamic';

const ProtocolsPage = async () => {
const protocols = await api.protocol.get.all.query();
return (
Expand Down
34 changes: 0 additions & 34 deletions components/ErrorAlert.tsx

This file was deleted.

9 changes: 9 additions & 0 deletions components/ErrorDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { PropsWithChildren } from 'react';

export const ErrorDetails = (props: PropsWithChildren) => {
return (
<div className="max-h-52 overflow-y-auto rounded-md border bg-primary text-sm text-white [&_pre]:whitespace-pre-wrap [&_pre]:p-6 ">
{props.children}
</div>
);
};
2 changes: 1 addition & 1 deletion components/ui/AlertDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const AlertDialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
className={cn('text-muted-foreground', className)}
{...props}
/>
));
Expand Down
2 changes: 1 addition & 1 deletion components/ui/ErrorDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ const ErrorDialog = ({
{description && (
<AlertDialogDescription>{description}</AlertDialogDescription>
)}
{additionalContent}
</AlertDialogHeader>
{additionalContent}
<AlertDialogFooter>
<AlertDialogAction className="bg-red-500" onClick={onConfirm}>
{confirmLabel}
Expand Down
53 changes: 53 additions & 0 deletions components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"use client"

import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"

import { cn } from "~/utils/shadcn"

const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName

const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
className={cn(
"relative rounded-full bg-border",
orientation === "vertical" && "flex-1"
)}
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName

export { ScrollArea, ScrollBar }
Loading

0 comments on commit ff91a03

Please sign in to comment.