From 249b292d437d9631f730a42e9007f121cb94ef43 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Fri, 8 May 2026 13:05:00 -0700 Subject: [PATCH 01/11] fix(dashboard): isolate tab routing and error boundaries Render only the active dashboard query tab and use hover-intent prefetch for tab links. Add route-level reset-aware error boundaries so tab and page failures stay scoped to their own segment. Co-authored-by: Cursor --- .../dashboard/[teamSlug]/account/error.tsx | 13 +++++ .../dashboard/[teamSlug]/billing/error.tsx | 13 +++++ .../[teamSlug]/billing/plan/error.tsx | 13 +++++ .../[teamSlug]/billing/plan/select/error.tsx | 13 +++++ src/app/dashboard/[teamSlug]/error.tsx | 13 +++++ .../dashboard/[teamSlug]/general/error.tsx | 13 +++++ src/app/dashboard/[teamSlug]/keys/error.tsx | 13 +++++ src/app/dashboard/[teamSlug]/limits/error.tsx | 13 +++++ .../dashboard/[teamSlug]/members/error.tsx | 13 +++++ .../sandboxes/(tabs)/@list/default.tsx | 3 -- .../sandboxes/(tabs)/@list/page.tsx | 24 --------- .../sandboxes/(tabs)/@monitoring/default.tsx | 3 -- .../sandboxes/(tabs)/@monitoring/page.tsx | 18 ------- .../[teamSlug]/sandboxes/(tabs)/error.tsx | 13 +++++ .../[teamSlug]/sandboxes/(tabs)/layout.tsx | 12 ++--- .../[teamSlug]/sandboxes/(tabs)/page.tsx | 53 ++++++++++++++++++- .../sandboxes/[sandboxId]/error.tsx | 13 +++++ .../[sandboxId]/filesystem/error.tsx | 13 +++++ .../sandboxes/[sandboxId]/logs/error.tsx | 13 +++++ .../[sandboxId]/monitoring/error.tsx | 13 +++++ .../templates/(tabs)/@builds/default.tsx | 3 -- .../templates/(tabs)/@builds/page.tsx | 11 ---- .../templates/(tabs)/@list/default.tsx | 3 -- .../templates/(tabs)/@list/page.tsx | 11 ---- .../[teamSlug]/templates/(tabs)/error.tsx | 13 +++++ .../[teamSlug]/templates/(tabs)/layout.tsx | 12 ++--- .../[teamSlug]/templates/(tabs)/page.tsx | 33 ++++++++++++ .../[templateId]/builds/[buildId]/error.tsx | 13 +++++ .../templates/[templateId]/error.tsx | 13 +++++ src/app/dashboard/[teamSlug]/usage/error.tsx | 13 +++++ .../dashboard/[teamSlug]/webhooks/error.tsx | 13 +++++ src/features/dashboard/shared/route-error.tsx | 37 +++++++++++++ src/ui/dashboard-tabs.tsx | 40 ++++++++++---- src/ui/error-indicator.tsx | 13 ++++- src/ui/error.tsx | 4 ++ 35 files changed, 422 insertions(+), 105 deletions(-) create mode 100644 src/app/dashboard/[teamSlug]/account/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/billing/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/billing/plan/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/billing/plan/select/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/general/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/keys/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/limits/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/members/error.tsx delete mode 100644 src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@list/default.tsx delete mode 100644 src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@list/page.tsx delete mode 100644 src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@monitoring/default.tsx delete mode 100644 src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@monitoring/page.tsx create mode 100644 src/app/dashboard/[teamSlug]/sandboxes/(tabs)/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/filesystem/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/logs/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/monitoring/error.tsx delete mode 100644 src/app/dashboard/[teamSlug]/templates/(tabs)/@builds/default.tsx delete mode 100644 src/app/dashboard/[teamSlug]/templates/(tabs)/@builds/page.tsx delete mode 100644 src/app/dashboard/[teamSlug]/templates/(tabs)/@list/default.tsx delete mode 100644 src/app/dashboard/[teamSlug]/templates/(tabs)/@list/page.tsx create mode 100644 src/app/dashboard/[teamSlug]/templates/(tabs)/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/templates/(tabs)/page.tsx create mode 100644 src/app/dashboard/[teamSlug]/templates/[templateId]/builds/[buildId]/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/templates/[templateId]/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/usage/error.tsx create mode 100644 src/app/dashboard/[teamSlug]/webhooks/error.tsx create mode 100644 src/features/dashboard/shared/route-error.tsx diff --git a/src/app/dashboard/[teamSlug]/account/error.tsx b/src/app/dashboard/[teamSlug]/account/error.tsx new file mode 100644 index 000000000..4d4ecd0c8 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/account/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function AccountPageError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/billing/error.tsx b/src/app/dashboard/[teamSlug]/billing/error.tsx new file mode 100644 index 000000000..ed22d82be --- /dev/null +++ b/src/app/dashboard/[teamSlug]/billing/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function BillingPageError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/billing/plan/error.tsx b/src/app/dashboard/[teamSlug]/billing/plan/error.tsx new file mode 100644 index 000000000..34f271d2c --- /dev/null +++ b/src/app/dashboard/[teamSlug]/billing/plan/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function BillingPlanPageError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/billing/plan/select/error.tsx b/src/app/dashboard/[teamSlug]/billing/plan/select/error.tsx new file mode 100644 index 000000000..1459303c4 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/billing/plan/select/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function BillingPlanSelectPageError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/error.tsx b/src/app/dashboard/[teamSlug]/error.tsx new file mode 100644 index 000000000..3d9b42590 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function TeamDashboardError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/general/error.tsx b/src/app/dashboard/[teamSlug]/general/error.tsx new file mode 100644 index 000000000..d05558725 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/general/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function GeneralPageError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/keys/error.tsx b/src/app/dashboard/[teamSlug]/keys/error.tsx new file mode 100644 index 000000000..553282320 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/keys/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function KeysPageError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/limits/error.tsx b/src/app/dashboard/[teamSlug]/limits/error.tsx new file mode 100644 index 000000000..7dccbca76 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/limits/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function LimitsPageError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/members/error.tsx b/src/app/dashboard/[teamSlug]/members/error.tsx new file mode 100644 index 000000000..e2df67c76 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/members/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function MembersPageError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@list/default.tsx b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@list/default.tsx deleted file mode 100644 index 052d11542..000000000 --- a/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@list/default.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function ListPage() { - return null -} diff --git a/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@list/page.tsx b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@list/page.tsx deleted file mode 100644 index b33a23310..000000000 --- a/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@list/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Suspense } from 'react' -import LoadingLayout from '@/features/dashboard/loading-layout' -import SandboxesTable from '@/features/dashboard/sandboxes/list/table' -import { HydrateClient, prefetch, trpc } from '@/trpc/server' - -export default async function ListPage({ - params, -}: PageProps<'/dashboard/[teamSlug]/sandboxes'>) { - const { teamSlug } = await params - - prefetch( - trpc.sandboxes.getSandboxes.queryOptions({ - teamSlug, - }) - ) - - return ( - - }> - - - - ) -} diff --git a/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@monitoring/default.tsx b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@monitoring/default.tsx deleted file mode 100644 index f566b2cd3..000000000 --- a/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@monitoring/default.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function MonitoringPage() { - return null -} diff --git a/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@monitoring/page.tsx b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@monitoring/page.tsx deleted file mode 100644 index a80be0b05..000000000 --- a/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/@monitoring/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { TeamMetricsCharts } from '@/features/dashboard/sandboxes/monitoring/charts/charts' -import SandboxesMonitoringHeader from '@/features/dashboard/sandboxes/monitoring/header' - -export default async function MonitoringPage({ - params, - searchParams, -}: PageProps<'/dashboard/[teamSlug]/sandboxes'> & { - searchParams: Promise<{ start?: string; end?: string }> -}) { - return ( -
- -
- -
-
- ) -} diff --git a/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/error.tsx b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/error.tsx new file mode 100644 index 000000000..45ac9f606 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function SandboxesTabsError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/layout.tsx b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/layout.tsx index ef434b39e..2c49c2e5c 100644 --- a/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/layout.tsx +++ b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/layout.tsx @@ -2,12 +2,8 @@ import { DashboardTab, DashboardTabs } from '@/ui/dashboard-tabs' import { ListIcon, TrendIcon } from '@/ui/primitives/icons' export default function SandboxesLayout({ - list, - monitoring, -}: LayoutProps<'/dashboard/[teamSlug]/sandboxes'> & { - list: React.ReactNode - monitoring: React.ReactNode -}) { + children, +}: LayoutProps<'/dashboard/[teamSlug]/sandboxes'>) { return ( } > - {monitoring} + {children} } > - {list} + {children} ) diff --git a/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/page.tsx b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/page.tsx index 08d7e9dc6..ad87ae566 100644 --- a/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/page.tsx +++ b/src/app/dashboard/[teamSlug]/sandboxes/(tabs)/page.tsx @@ -1,3 +1,52 @@ -export default function SandboxesTabsPage() { - return null // this page is not used, it's just to satisfy the parallel routes +import { Suspense } from 'react' +import LoadingLayout from '@/features/dashboard/loading-layout' +import SandboxesTable from '@/features/dashboard/sandboxes/list/table' +import { TeamMetricsCharts } from '@/features/dashboard/sandboxes/monitoring/charts/charts' +import SandboxesMonitoringHeader from '@/features/dashboard/sandboxes/monitoring/header' +import { HydrateClient, prefetch, trpc } from '@/trpc/server' + +type SandboxesSearchParams = { + tab?: string + start?: string + end?: string +} + +export default async function SandboxesTabsPage({ + params, + searchParams, +}: PageProps<'/dashboard/[teamSlug]/sandboxes'> & { + searchParams: Promise +}) { + const { tab, start, end } = await searchParams + const activeTab = tab === 'list' ? 'list' : 'monitoring' + + if (activeTab === 'list') { + const { teamSlug } = await params + + prefetch( + trpc.sandboxes.getSandboxes.queryOptions({ + teamSlug, + }) + ) + + return ( + + }> + + + + ) + } + + return ( +
+ +
+ +
+
+ ) } diff --git a/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/error.tsx b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/error.tsx new file mode 100644 index 000000000..c3f1d88a6 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function SandboxDetailsError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/filesystem/error.tsx b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/filesystem/error.tsx new file mode 100644 index 000000000..2c0c439e1 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/filesystem/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function SandboxFilesystemPageError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/logs/error.tsx b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/logs/error.tsx new file mode 100644 index 000000000..556a0c9b1 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/logs/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function SandboxLogsPageError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/monitoring/error.tsx b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/monitoring/error.tsx new file mode 100644 index 000000000..2c76b0087 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/sandboxes/[sandboxId]/monitoring/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function SandboxMonitoringPageError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/templates/(tabs)/@builds/default.tsx b/src/app/dashboard/[teamSlug]/templates/(tabs)/@builds/default.tsx deleted file mode 100644 index 86b9e9a38..000000000 --- a/src/app/dashboard/[teamSlug]/templates/(tabs)/@builds/default.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Default() { - return null -} diff --git a/src/app/dashboard/[teamSlug]/templates/(tabs)/@builds/page.tsx b/src/app/dashboard/[teamSlug]/templates/(tabs)/@builds/page.tsx deleted file mode 100644 index 3e280c132..000000000 --- a/src/app/dashboard/[teamSlug]/templates/(tabs)/@builds/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import BuildsHeader from '@/features/dashboard/templates/builds/header' -import BuildsTable from '@/features/dashboard/templates/builds/table' - -export default function BuildsPage() { - return ( -
- - -
- ) -} diff --git a/src/app/dashboard/[teamSlug]/templates/(tabs)/@list/default.tsx b/src/app/dashboard/[teamSlug]/templates/(tabs)/@list/default.tsx deleted file mode 100644 index 86b9e9a38..000000000 --- a/src/app/dashboard/[teamSlug]/templates/(tabs)/@list/default.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Default() { - return null -} diff --git a/src/app/dashboard/[teamSlug]/templates/(tabs)/@list/page.tsx b/src/app/dashboard/[teamSlug]/templates/(tabs)/@list/page.tsx deleted file mode 100644 index a2fb9f4be..000000000 --- a/src/app/dashboard/[teamSlug]/templates/(tabs)/@list/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Suspense } from 'react' -import LoadingLayout from '@/features/dashboard/loading-layout' -import TemplatesTable from '@/features/dashboard/templates/list/table' - -export default async function ListPage() { - return ( - }> - - - ) -} diff --git a/src/app/dashboard/[teamSlug]/templates/(tabs)/error.tsx b/src/app/dashboard/[teamSlug]/templates/(tabs)/error.tsx new file mode 100644 index 000000000..2a865a89f --- /dev/null +++ b/src/app/dashboard/[teamSlug]/templates/(tabs)/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function TemplatesTabsError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/templates/(tabs)/layout.tsx b/src/app/dashboard/[teamSlug]/templates/(tabs)/layout.tsx index 292e65e25..8189c767e 100644 --- a/src/app/dashboard/[teamSlug]/templates/(tabs)/layout.tsx +++ b/src/app/dashboard/[teamSlug]/templates/(tabs)/layout.tsx @@ -2,12 +2,8 @@ import { DashboardTab, DashboardTabs } from '@/ui/dashboard-tabs' import { BuildIcon, ListIcon } from '@/ui/primitives/icons' export default function TemplatesLayout({ - list, - builds, -}: LayoutProps<'/dashboard/[teamSlug]/templates'> & { - list: React.ReactNode - builds: React.ReactNode -}) { + children, +}: LayoutProps<'/dashboard/[teamSlug]/templates'>) { return ( } > - {list} + {children} } > - {builds} + {children} ) diff --git a/src/app/dashboard/[teamSlug]/templates/(tabs)/page.tsx b/src/app/dashboard/[teamSlug]/templates/(tabs)/page.tsx new file mode 100644 index 000000000..d93593569 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/templates/(tabs)/page.tsx @@ -0,0 +1,33 @@ +import { Suspense } from 'react' +import LoadingLayout from '@/features/dashboard/loading-layout' +import BuildsHeader from '@/features/dashboard/templates/builds/header' +import BuildsTable from '@/features/dashboard/templates/builds/table' +import TemplatesTable from '@/features/dashboard/templates/list/table' + +type TemplatesSearchParams = { + tab?: string +} + +export default async function TemplatesTabsPage({ + searchParams, +}: PageProps<'/dashboard/[teamSlug]/templates'> & { + searchParams: Promise +}) { + const { tab } = await searchParams + const activeTab = tab === 'builds' ? 'builds' : 'list' + + if (activeTab === 'builds') { + return ( +
+ + +
+ ) + } + + return ( + }> + + + ) +} diff --git a/src/app/dashboard/[teamSlug]/templates/[templateId]/builds/[buildId]/error.tsx b/src/app/dashboard/[teamSlug]/templates/[templateId]/builds/[buildId]/error.tsx new file mode 100644 index 000000000..58a9a1717 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/templates/[templateId]/builds/[buildId]/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function TemplateBuildDetailsError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/templates/[templateId]/error.tsx b/src/app/dashboard/[teamSlug]/templates/[templateId]/error.tsx new file mode 100644 index 000000000..de47bcc1e --- /dev/null +++ b/src/app/dashboard/[teamSlug]/templates/[templateId]/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function TemplateDetailsError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/usage/error.tsx b/src/app/dashboard/[teamSlug]/usage/error.tsx new file mode 100644 index 000000000..9bbb24a17 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/usage/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function UsagePageError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/app/dashboard/[teamSlug]/webhooks/error.tsx b/src/app/dashboard/[teamSlug]/webhooks/error.tsx new file mode 100644 index 000000000..780f8e741 --- /dev/null +++ b/src/app/dashboard/[teamSlug]/webhooks/error.tsx @@ -0,0 +1,13 @@ +'use client' + +import { DashboardRouteError } from '@/features/dashboard/shared/route-error' + +export default function WebhooksPageError({ + error, + reset, +}: { + error: Error & { digest?: string } + reset: () => void +}) { + return +} diff --git a/src/features/dashboard/shared/route-error.tsx b/src/features/dashboard/shared/route-error.tsx new file mode 100644 index 000000000..64996c0bb --- /dev/null +++ b/src/features/dashboard/shared/route-error.tsx @@ -0,0 +1,37 @@ +'use client' + +import { usePathname, useSearchParams } from 'next/navigation' +import { useEffect, useMemo, useRef } from 'react' +import ErrorBoundary from '@/ui/error' + +interface DashboardRouteErrorProps { + error: Error & { digest?: string } + reset: () => void +} + +export function DashboardRouteError({ error, reset }: DashboardRouteErrorProps) { + const pathname = usePathname() + const searchParams = useSearchParams() + + const routeKey = useMemo( + () => `${pathname}?${searchParams.toString()}`, + [pathname, searchParams] + ) + + const previousRouteKeyRef = useRef(routeKey) + + useEffect(() => { + if (previousRouteKeyRef.current !== routeKey) { + previousRouteKeyRef.current = routeKey + reset() + } + }, [routeKey, reset]) + + return ( + + ) +} diff --git a/src/ui/dashboard-tabs.tsx b/src/ui/dashboard-tabs.tsx index bd1fe5db2..15e0a97e1 100644 --- a/src/ui/dashboard-tabs.tsx +++ b/src/ui/dashboard-tabs.tsx @@ -1,6 +1,5 @@ 'use client' -import Link from 'next/link' import { usePathname, useSearchParams } from 'next/navigation' import { memo, @@ -10,6 +9,7 @@ import { useMemo, } from 'react' import { cn } from '@/lib/utils' +import { HoverPrefetchLink } from '@/ui/hover-prefetch-link' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/primitives/tabs' type DashboardTabElement = ReactElement @@ -34,14 +34,18 @@ function DashboardTabsComponent({ const searchParams = useSearchParams() const pathname = usePathname() + const tabChildren = useMemo( + () => (Array.isArray(children) ? children : [children]), + [children] + ) + const tabs = useMemo(() => { - const tabChildren = Array.isArray(children) ? children : [children] return tabChildren.map((child) => ({ id: child.props.id, label: child.props.label, icon: child.props.icon, })) - }, [children]) + }, [tabChildren]) const basePath = useMemo(() => { if (type === 'query') return pathname @@ -56,11 +60,19 @@ function DashboardTabsComponent({ ) const activeTabId = useMemo(() => { - if (type === 'query') { - const defaultTabId = tabs[0]?.id - return searchParams.get('tab') || defaultTabId + const defaultTabId = tabs[0]?.id + if (!defaultTabId) { + return undefined } - return tabs.find((tab) => pathname.endsWith(tab.id))?.id || tabs[0]?.id + + const requestedTabId = + type === 'query' + ? searchParams.get('tab') || defaultTabId + : tabs.find((tab) => pathname.endsWith(tab.id))?.id || defaultTabId + + return tabs.some((tab) => tab.id === requestedTabId) + ? requestedTabId + : defaultTabId }, [type, tabs, searchParams, pathname]) const tabsWithHrefs = useMemo( @@ -68,6 +80,14 @@ function DashboardTabsComponent({ [tabs, hrefForId] ) + const activeTab = useMemo( + () => + tabChildren.find((child) => child.props.id === activeTabId) || + tabChildren[0] || + null, + [tabChildren, activeTabId] + ) + const tabTriggers = tabsWithHrefs.map((tab) => ( - + {tab.icon} {tab.label} - + )) @@ -103,7 +123,7 @@ function DashboardTabsComponent({ )} - {children} + {activeTab} ) } diff --git a/src/ui/error-indicator.tsx b/src/ui/error-indicator.tsx index cf40fc404..7312578b1 100644 --- a/src/ui/error-indicator.tsx +++ b/src/ui/error-indicator.tsx @@ -20,6 +20,7 @@ interface ErrorIndicatorProps { message?: string className?: string children?: React.ReactNode + onRetry?: () => void } export function ErrorIndicator({ @@ -28,6 +29,7 @@ export function ErrorIndicator({ message, className, children, + onRetry, }: ErrorIndicatorProps) { const router = useRouter() const [isPending, startTransition] = useTransition() @@ -51,7 +53,16 @@ export function ErrorIndicator({