From a7c64edfc066ed2fe3c8c62f01e0adfb0fb873ee Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Sun, 12 Apr 2026 16:05:56 -0400 Subject: [PATCH] Add contextual error screen that preserves dashboard layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Error screens now render inside the card content area instead of replacing the entire dashboard shell. Errors show friendly messages based on type (403 → configure access prompt, 404, rate limit, network, timeout) with GitHub's error detail. Also renames QuickHub references to DiffKit. --- .../layouts/dashboard-error-screen.tsx | 182 ++++++++++++++++++ .../src/components/layouts/error-screen.tsx | 40 +--- .../pulls/detail/pull-detail-activity.tsx | 2 +- apps/dashboard/src/lib/github.server.test.ts | 2 +- apps/dashboard/src/lib/github.server.ts | 2 +- apps/dashboard/src/router.tsx | 2 + packages/icons/src/index.ts | 2 + 7 files changed, 194 insertions(+), 38 deletions(-) create mode 100644 apps/dashboard/src/components/layouts/dashboard-error-screen.tsx diff --git a/apps/dashboard/src/components/layouts/dashboard-error-screen.tsx b/apps/dashboard/src/components/layouts/dashboard-error-screen.tsx new file mode 100644 index 0000000..fce7310 --- /dev/null +++ b/apps/dashboard/src/components/layouts/dashboard-error-screen.tsx @@ -0,0 +1,182 @@ +import { + AlertCircleIcon, + LockIcon, + RefreshCwIcon, + SearchIcon, + WifiOffIcon, +} from "@diffkit/icons"; +import { Button } from "@diffkit/ui/components/button"; +import { cn } from "@diffkit/ui/lib/utils"; +import { type ErrorComponentProps, useRouter } from "@tanstack/react-router"; +import type { ComponentType } from "react"; +import { useShowOrgSetupQueryState } from "#/lib/github-access-dialog-query"; +import { openGitHubAccessPrompt } from "#/lib/github-access-modal-store"; + +type ErrorInfo = { + icon: ComponentType<{ size?: number; strokeWidth?: number }>; + iconClassName: string; + title: string; + description: string; + action: "retry" | "configure-access"; +}; + +function getErrorInfo(error: Error): ErrorInfo { + const msg = error.message; + const lower = msg.toLowerCase(); + + if (lower.includes("rate limit") || lower.includes("429")) { + return { + icon: AlertCircleIcon, + iconClassName: "bg-amber-500/10 text-amber-500", + title: "Rate limit reached", + description: + "You've made too many requests. Wait a moment and try again.", + action: "retry", + }; + } + + if ( + lower.includes("403") || + lower.includes("forbidden") || + lower.includes("not accessible by integration") || + lower.includes("insufficient permissions") + ) { + return { + icon: LockIcon, + iconClassName: "bg-amber-500/10 text-amber-500", + title: "Access not configured", + description: + "DiffKit doesn't have access to this resource. Add the repository or organization in your GitHub app settings.", + action: "configure-access", + }; + } + + if (lower.includes("404") || lower.includes("not found")) { + return { + icon: SearchIcon, + iconClassName: "bg-muted-foreground/10 text-muted-foreground", + title: "Not found", + description: + "This resource doesn't exist or you don't have access to it.", + action: "retry", + }; + } + + if ( + lower.includes("network") || + lower.includes("fetch failed") || + lower.includes("econnrefused") || + lower.includes("enotfound") || + lower.includes("failed to fetch") + ) { + return { + icon: WifiOffIcon, + iconClassName: "bg-amber-500/10 text-amber-500", + title: "Connection failed", + description: + "Could not reach the server. Check your internet connection and try again.", + action: "retry", + }; + } + + if (lower.includes("timeout") || lower.includes("timed out")) { + return { + icon: AlertCircleIcon, + iconClassName: "bg-amber-500/10 text-amber-500", + title: "Request timed out", + description: + "The request took too long to complete. Try again in a moment.", + action: "retry", + }; + } + + return { + icon: AlertCircleIcon, + iconClassName: "bg-destructive/10 text-destructive", + title: "Something went wrong", + description: + msg || + "An unexpected error occurred. Please try again or refresh the page.", + action: "retry", + }; +} + +/** Strip the trailing ` - GET https://…` suffix that octokit appends. */ +function cleanErrorMessage(msg: string): string | null { + if (!msg) return null; + const cleaned = msg + .replace(/\s*-\s+(GET|POST|PUT|PATCH|DELETE|HEAD)\s+https?:\/\/\S+$/i, "") + .trim(); + return cleaned || null; +} + +export function DashboardErrorScreen({ error, reset }: ErrorComponentProps) { + const router = useRouter(); + const { + icon: Icon, + iconClassName, + title, + description, + action, + } = getErrorInfo(error); + const detail = cleanErrorMessage(error.message); + + return ( +
+
+
+ +
+ +
+

{title}

+

+ {description} +

+
+ + {detail && ( +

+ {detail} +

+ )} + +
+ {action === "configure-access" ? : null} + +
+
+
+ ); +} + +function ConfigureAccessButton() { + const [, setShowOrgSetup] = useShowOrgSetupQueryState(); + + return ( + + ); +} diff --git a/apps/dashboard/src/components/layouts/error-screen.tsx b/apps/dashboard/src/components/layouts/error-screen.tsx index 7dab3e6..324a509 100644 --- a/apps/dashboard/src/components/layouts/error-screen.tsx +++ b/apps/dashboard/src/components/layouts/error-screen.tsx @@ -1,40 +1,10 @@ -import { AlertCircleIcon, RefreshCwIcon } from "@diffkit/icons"; -import { Button } from "@diffkit/ui/components/button"; -import { type ErrorComponentProps, useRouter } from "@tanstack/react-router"; - -export function ErrorScreen({ reset }: ErrorComponentProps) { - const router = useRouter(); +import type { ErrorComponentProps } from "@tanstack/react-router"; +import { DashboardErrorScreen } from "./dashboard-error-screen"; +export function ErrorScreen(props: ErrorComponentProps) { return ( -
-
-
- -
- -
-

- Something went wrong -

-

- An unexpected error occurred. Please try again or refresh the page. -

-
- -
- -
-
+
+
); } diff --git a/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx b/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx index f5ce918..03927a1 100644 --- a/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx +++ b/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx @@ -523,7 +523,7 @@ function ReviewsSection({ repo, pullNumber, reviewId: review.id, - message: "Dismissed via QuickHub", + message: "Dismissed via DiffKit", }, }) .then((result) => { diff --git a/apps/dashboard/src/lib/github.server.test.ts b/apps/dashboard/src/lib/github.server.test.ts index 3739bab..283936b 100644 --- a/apps/dashboard/src/lib/github.server.test.ts +++ b/apps/dashboard/src/lib/github.server.test.ts @@ -93,7 +93,7 @@ describe("getGitHubClient", () => { }; expect(options.auth).toBe("github-token"); - expect(options.userAgent).toBe("quickhub-dashboard"); + expect(options.userAgent).toBe("diffkit-dashboard"); expect(options.retry).toEqual({ enabled: true }); expect(options.throttle.enabled).toBe(true); expect(options.throttle.id).toBe("github-user:user-123"); diff --git a/apps/dashboard/src/lib/github.server.ts b/apps/dashboard/src/lib/github.server.ts index c1024bb..b86a9bf 100644 --- a/apps/dashboard/src/lib/github.server.ts +++ b/apps/dashboard/src/lib/github.server.ts @@ -7,7 +7,7 @@ import { } from "./github-app.server"; import { configureGitHubRequestPolicies } from "./github-request-policy"; -const GITHUB_CLIENT_USER_AGENT = "quickhub-dashboard"; +const GITHUB_CLIENT_USER_AGENT = "diffkit-dashboard"; const GITHUB_SECONDARY_RATE_LIMIT_FALLBACK_SECONDS = 60; type GitHubThrottleRequestOptions = { diff --git a/apps/dashboard/src/router.tsx b/apps/dashboard/src/router.tsx index 6e50999..fe1c77d 100644 --- a/apps/dashboard/src/router.tsx +++ b/apps/dashboard/src/router.tsx @@ -1,5 +1,6 @@ import { createRouter as createTanStackRouter } from "@tanstack/react-router"; import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query"; +import { DashboardErrorScreen } from "#/components/layouts/dashboard-error-screen"; import { AppQueryClientProvider, createAppQueryClient, @@ -17,6 +18,7 @@ export function getRouter() { defaultPreload: "intent", defaultPreloadStaleTime: 0, defaultPendingMs: 0, + defaultErrorComponent: DashboardErrorScreen, Wrap: ({ children }) => ( {children} diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts index 3a380e1..57622ba 100644 --- a/packages/icons/src/index.ts +++ b/packages/icons/src/index.ts @@ -44,6 +44,7 @@ export { Link02Icon as ExternalLinkIcon, Loading03Icon as LoaderCircleIcon, Location01Icon as LocationIcon, + LockIcon, Mail01Icon as MailIcon, Message01Icon as MessageIcon, Moon02Icon as MoonIcon, @@ -64,6 +65,7 @@ export { UserCircleIcon, UserGroupIcon as FollowersIcon, ViewIcon, + WifiDisconnected01Icon as WifiOffIcon, } from "@hugeicons/react"; export { GitHubLogo, GitHubWordmarkLogo } from "./brand-logos"; export { PenIcon } from "./pen-icon";