Skip to content

Commit 61e56f3

Browse files
committed
next-analyze: improve network error visuals
1 parent 7117ffa commit 61e56f3

File tree

6 files changed

+86
-23
lines changed

6 files changed

+86
-23
lines changed

apps/bundle-analyzer/app/page.tsx

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type React from 'react'
44
import { useEffect, useMemo, useRef, useState } from 'react'
55
import useSWR from 'swr'
66
import { ImportChain } from '@/components/import-chain'
7+
import { ErrorState } from '@/components/error-state'
78
import {
89
RouteTypeahead,
910
type RouteTypeaheadRef,
@@ -15,7 +16,7 @@ import { Skeleton, TreemapSkeleton } from '@/components/ui/skeleton'
1516
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
1617
import { AnalyzeData, ModulesData } from '@/lib/analyze-data'
1718
import { SpecialModule } from '@/lib/types'
18-
import { getSpecialModuleType } from '@/lib/utils'
19+
import { getSpecialModuleType, fetchStrict } from '@/lib/utils'
1920

2021
function formatBytes(bytes: number): string {
2122
if (bytes === 0) return '0 B'
@@ -164,12 +165,6 @@ export default function Home() {
164165
</div>
165166

166167
<div className="basis-2/3 flex justify-end">
167-
{error && (
168-
<div className="text-sm text-red-600 font-medium">
169-
Error: {error?.message}
170-
</div>
171-
)}
172-
173168
{analyzeData && (
174169
<>
175170
<ToggleGroup
@@ -227,7 +222,9 @@ export default function Home() {
227222
</div>
228223

229224
<div className="flex-1 flex min-h-0">
230-
{isAnyLoading ? (
225+
{error && !analyzeData ? (
226+
<ErrorState error={error} />
227+
) : isAnyLoading ? (
231228
<>
232229
<div className="flex-1 min-w-0 p-4 bg-background">
233230
<TreemapSkeleton />
@@ -321,8 +318,8 @@ export default function Home() {
321318
{specialModuleType ===
322319
SpecialModule.POLYFILL_NOMODULE ? (
323320
<>
324-
. <code>polyfill-nomodule.js</code> is only sent
325-
to legacy browsers.
321+
. <code>polyfill-nomodule.js</code> is only
322+
sent to legacy browsers.
326323
</>
327324
) : null}
328325
</dd>
@@ -414,12 +411,3 @@ async function fetchModulesData(url: string): Promise<ModulesData> {
414411
const resp = await fetchStrict(url)
415412
return new ModulesData(await resp.arrayBuffer())
416413
}
417-
418-
function fetchStrict(url: string): Promise<Response> {
419-
return fetch(url).then((res) => {
420-
if (!res.ok) {
421-
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`)
422-
}
423-
return res
424-
})
425-
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use client'
2+
3+
import { RefreshCw, AlertTriangle } from 'lucide-react'
4+
import { NetworkError } from '@/lib/errors'
5+
6+
interface ErrorStateProps {
7+
error: unknown
8+
onRetry?: () => void
9+
}
10+
11+
export function ErrorState({ error }: ErrorStateProps) {
12+
const isNetwork = error instanceof NetworkError
13+
const title = isNetwork ? 'Server Connection Lost' : 'Error'
14+
const message = isNetwork
15+
? 'Unable to connect to the bundle analyzer server. Please ensure the server is running and try again.'
16+
: ((error as any)?.message ?? 'An unexpected error occurred.')
17+
18+
return (
19+
<div className="flex-1 flex items-center justify-center">
20+
<div className="text-center space-y-4 max-w-md px-6">
21+
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-destructive/10 text-destructive mb-2">
22+
<AlertTriangle className="w-8 h-8" />
23+
</div>
24+
<div>
25+
<h3 className="text-lg font-semibold text-foreground mb-2">
26+
{title}
27+
</h3>
28+
<p className="text-sm text-muted-foreground mb-4">{message}</p>
29+
<button
30+
onClick={() => {
31+
window.location.reload()
32+
}}
33+
className="inline-flex items-center gap-2 px-4 py-2 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors text-sm font-medium"
34+
>
35+
<RefreshCw className="w-4 h-4" />
36+
Retry Connection
37+
</button>
38+
</div>
39+
</div>
40+
</div>
41+
)
42+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { ImportChain } from './import-chain'
22
export { RouteTypeahead } from './route-typeahead'
33
export { TreemapVisualizer } from './treemap-visualizer'
4+
export { ErrorState } from './error-state'

apps/bundle-analyzer/components/route-typeahead.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
PopoverTrigger,
1919
} from '@/components/ui/popover'
2020
import { cn, jsonFetcher } from '@/lib/utils'
21+
import { NetworkError } from '@/lib/errors'
2122

2223
interface RouteTypeaheadProps {
2324
selectedRoute: string | null
@@ -55,8 +56,13 @@ export const RouteTypeahead = forwardRef<
5556

5657
if (error) {
5758
return (
58-
<div className="flex max-w-full text-red-600">
59-
Failed to load routes manifest: {error.message}
59+
<div className="flex items-center gap-2 px-3 py-2 rounded-md bg-destructive/10 border border-destructive/20 text-destructive text-sm max-w-full">
60+
<span className="font-medium"></span>
61+
<span className="truncate">
62+
{error instanceof NetworkError
63+
? 'Unable to connect to server'
64+
: error.message}
65+
</span>
6066
</div>
6167
)
6268
}

apps/bundle-analyzer/lib/errors.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export class NetworkError extends Error {
2+
constructor(message: string, options?: { cause?: unknown }) {
3+
super(message)
4+
this.name = 'NetworkError'
5+
// Preserve error cause when supported
6+
if (options && 'cause' in options) {
7+
;(this as any).cause = options.cause
8+
}
9+
}
10+
}

apps/bundle-analyzer/lib/utils.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
11
import { type ClassValue, clsx } from 'clsx'
22
import { twMerge } from 'tailwind-merge'
33
import { SpecialModule } from './types'
4+
import { NetworkError } from './errors'
45
import { AnalyzeData } from './analyze-data'
56

67
export function cn(...inputs: ClassValue[]) {
78
return twMerge(clsx(inputs))
89
}
910

10-
export function jsonFetcher<T>(url: string): Promise<T> {
11-
return fetch(url).then((res) => res.json())
11+
export async function fetchStrict(url: string): Promise<Response> {
12+
let res: Response
13+
try {
14+
res = await fetch(url)
15+
} catch (err) {
16+
throw new NetworkError(`Failed to fetch ${url}`, { cause: err })
17+
}
18+
19+
if (!res.ok) {
20+
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`)
21+
}
22+
return res
23+
}
24+
25+
export async function jsonFetcher<T>(url: string): Promise<T> {
26+
const res = await fetchStrict(url)
27+
return res.json() as Promise<T>
1228
}
1329

1430
export function getSpecialModuleType(

0 commit comments

Comments
 (0)