Skip to content

Commit

Permalink
feat: update error component, wrap Error Boundary modal content (#129)
Browse files Browse the repository at this point in the history
* feat: update error component, wrapped modal content

Signed-off-by: Innei <i@innei.in>

* fix: 404 entry

Signed-off-by: Innei <i@innei.in>

* fix: update

Signed-off-by: Innei <i@innei.in>

---------

Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei authored Jul 11, 2024
1 parent 454700a commit 2683d3a
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 89 deletions.
1 change: 1 addition & 0 deletions icons/mgc/bug_cute_re.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"description": "An Electron application with React and TypeScript",
"author": "example.com",
"license": "UNLICENSED",
"homepage": "https://electron-vite.org",
"homepage": "https://dev.follow.is",
"repository": {
"url": "https://github.com/RSSNext/follow",
"type": "git"
Expand Down
1 change: 0 additions & 1 deletion src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { handlers } from "./tipc"

function App() {
useDark()

useEffect(() => {
const cleanup = handlers?.invalidateQuery.listen((queryKey) => {
queryClient.invalidateQueries({
Expand Down
26 changes: 13 additions & 13 deletions src/renderer/src/components/common/AppErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import type { FallbackRender } from "@sentry/react"
import { ErrorBoundary } from "@sentry/react"
import type { FC, PropsWithChildren } from "react"
import { useCallback } from "react"
import { createElement, useCallback } from "react"

import type { ErrorComponentType } from "../errors"
import { getErrorFallback } from "../errors"

export interface AppErrorBoundaryProps extends PropsWithChildren {
height?: number | string
errorType: ErrorComponentType
}

export const AppErrorBoundary: FC<AppErrorBoundaryProps> = (props) => {
export const AppErrorBoundary: FC<AppErrorBoundaryProps> = ({
errorType,
children,
}) => {
const fallbackRender: FallbackRender = useCallback(
(fallbackProps) => (
<ErrorFallback {...fallbackProps} height={props.height} />
),
[props.height],
(fallbackProps) =>
createElement(getErrorFallback(errorType), fallbackProps),
[errorType],
)

const onError = useCallback((error: unknown, componentStack?: string) => {
Expand All @@ -21,13 +27,7 @@ export const AppErrorBoundary: FC<AppErrorBoundaryProps> = (props) => {

return (
<ErrorBoundary fallback={fallbackRender} onError={onError}>
{props.children}
{children}
</ErrorBoundary>
)
}

export type AppErrorFallbackProps = Parameters<FallbackRender>[0] & {
height?: number | string
}

const ErrorFallback = (_props: AppErrorFallbackProps) => <div>App has crashed</div>
96 changes: 28 additions & 68 deletions src/renderer/src/components/common/ErrorElement.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { repository } from "@pkg"
import pkg, { repository } from "@pkg"
import { attachOpenInEditor } from "@renderer/lib/dev"
import { clearLocalPersistStoreData } from "@renderer/store/utils/clear"
import { useEffect, useRef } from "react"
import { isRouteErrorResponse, useRouteError } from "react-router-dom"

import { Logo } from "../icons/logo"
import { StyledButton } from "../ui/button"

export function ErrorElement() {
Expand Down Expand Up @@ -37,13 +39,17 @@ export function ErrorElement() {
}

return (
<div className="select-text p-8">
<div className="m-auto flex min-h-full max-w-prose select-text flex-col p-8 pt-12">
<div className="drag-region fixed inset-x-0 top-0 h-12" />

<h2 className="mt-12 text-2xl">Unexpected Application Error!</h2>
<div className="center flex flex-col">
<i className="i-mgc-bug-cute-re size-12 text-red-400" />
<h2 className="mt-12 text-2xl">
Sorry, the app has encountered an error
</h2>
</div>
<h3 className="text-xl">{message}</h3>
{import.meta.env.DEV && stack ? (
<div className="mt-4 cursor-text overflow-auto whitespace-pre rounded-md bg-red-50 p-4 font-mono text-sm text-red-600">
<div className="mt-4 cursor-text overflow-auto whitespace-pre rounded-md bg-red-50 p-4 text-left font-mono text-sm text-red-600">
{attachOpenInEditor(stack)}
</div>
) : null}
Expand Down Expand Up @@ -71,79 +77,33 @@ export function ErrorElement() {
<p className="mt-8">
Still having this issue? Please give feedback in Github, thanks!
<a
className="ml-2 cursor-pointer text-theme-accent-400/80 duration-200 hover:text-theme-accent"
className="ml-2 cursor-pointer text-theme-accent-500 duration-200 hover:text-theme-accent"
href={`${repository.url}/issues/new?title=${encodeURIComponent(
`Error: ${message}`,
)}&body=${encodeURIComponent(
`### Error\n\n${message}\n\n### Stack\n\n\`\`\`\n${stack}\n\`\`\``,
)}`}
)}&label=bug`}
target="_blank"
rel="noreferrer"
>
Submit Issue
</a>
</p>
<div className="grow" />
<footer className="center mt-12 flex gap-2">
Powered by
{" "}
<Logo className="size-5" />
{" "}
<a
href={pkg.homepage}
className="cursor-pointer font-bold text-theme-accent"
target="_blank"
rel="noreferrer"
>
{APP_NAME}
</a>
</footer>
</div>
)
}
const attachOpenInEditor = (stack: string) => {
const lines = stack.split("\n")
return lines.map((line) => {
// A line like this: at App (http://localhost:5173/src/App.tsx?t=1720527056591:41:9)
// Find the `localhost` part and open the file in the editor
if (!line.includes("at ")) {
return line
}
const match = line.match(/(http:\/\/localhost:\d+\/[^:]+):(\d+):(\d+)/)

if (match) {
const [o] = match

// Find `@fs/`
// Like: `http://localhost:5173/@fs/Users/innei/git/work/rss3/follow/node_modules/.vite/deps/chunk-RPCDYKBN.js?v=757920f2:11548:26`
const realFsPath = o.split("@fs")[1]

if (realFsPath) {
return (
// Delete `v=` hash, like `v=757920f2`
<div
className="cursor-pointer"
key={line}
onClick={openInEditor.bind(
null,
realFsPath.replace(/\?v=[a-f0-9]+/, ""),
)}
>
{line}
</div>
)
} else {
// at App (http://localhost:5173/src/App.tsx?t=1720527056591:41:9)
const srcFsPath = o.split("/src")[1]

if (srcFsPath) {
const fs = srcFsPath.replace(/\?t=[a-f0-9]+/, "")

return (
<div
className="cursor-pointer"
key={line}
onClick={openInEditor.bind(
null,
`${APP_DEV_CWD}/src/renderer/src${fs}`,
)}
>
{line}
</div>
)
}
}
}

return line
})
}
// http://localhost:5173/src/App.tsx?t=1720527056591:41:9
const openInEditor = (file: string) => {
fetch(`/__open-in-editor?file=${encodeURIComponent(`${file}`)}`)
}
70 changes: 70 additions & 0 deletions src/renderer/src/components/errors/ModalError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { repository } from "@pkg"
import { attachOpenInEditor } from "@renderer/lib/dev"
import type { FallbackRender } from "@sentry/react"
import type { FC } from "react"

import { m } from "../common/Motion"
import { StyledButton } from "../ui/button"
import { useCurrentModal } from "../ui/modal"
import { parseError } from "./helper"

export const ModalErrorFallback: FC<Parameters<FallbackRender>[0]> = (
props,
) => {
const { message, stack } = parseError(props.error)
const modal = useCurrentModal()
return (
<m.div
className="flex flex-col items-center justify-center rounded-md bg-theme-modal-background-opaque p-2"
exit={{
opacity: 0,
scale: 0.9,
}}
>
<div className="m-auto max-w-prose text-center">
<div className="mb-4">
<i className="i-mgc-bug-cute-re text-4xl text-red-500" />
</div>
<div className="text-lg font-bold">{message}</div>
{import.meta.env.DEV && stack ? (
<div className="mt-4 cursor-text overflow-auto whitespace-pre rounded-md bg-red-50 p-4 text-left font-mono text-sm text-red-600">
{attachOpenInEditor(stack)}
</div>
) : null}

<p className="my-8">
The App has a temporary problem, click the button below to try
reloading the app or another solution?
</p>

<div className="center gap-4">
<StyledButton onClick={() => modal.dismiss()} variant="outline">
Close Modal
</StyledButton>
<StyledButton
onClick={() => window.location.reload()}
variant="outline"
>
Reload
</StyledButton>
</div>

<p className="mt-8">
Still having this issue? Please give feedback in Github, thanks!
<a
className="ml-2 cursor-pointer text-theme-accent-500 duration-200 hover:text-theme-accent"
href={`${repository.url}/issues/new?title=${encodeURIComponent(
`Error: ${message}`,
)}&body=${encodeURIComponent(
`### Error\n\n${message}\n\n### Stack\n\n\`\`\`\n${stack}\n\`\`\``,
)}&label=bug`}
target="_blank"
rel="noreferrer"
>
Submit Issue
</a>
</p>
</div>
</m.div>
)
}
13 changes: 13 additions & 0 deletions src/renderer/src/components/errors/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const parseError = (error: unknown): { message?: string, stack?: string } => {
if (error instanceof Error) {
return {
message: error.message,
stack: error.stack,
}
} else {
return {
message: String(error),
stack: undefined,
}
}
}
11 changes: 11 additions & 0 deletions src/renderer/src/components/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ModalErrorFallback } from "./ModalError"

export enum ErrorComponentType {
Modal = "Modal",
}

export const ErrorFallbackMap = {
[ErrorComponentType.Modal]: ModalErrorFallback,
}

export const getErrorFallback = (type: ErrorComponentType) => ErrorFallbackMap[type]
6 changes: 5 additions & 1 deletion src/renderer/src/components/ui/modal/stacked/modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as Dialog from "@radix-ui/react-dialog"
import { useUISettingKey } from "@renderer/atoms/settings/ui"
import { AppErrorBoundary } from "@renderer/components/common/AppErrorBoundary"
import { m } from "@renderer/components/common/Motion"
import { ErrorComponentType } from "@renderer/components/errors"
import { stopPropagation } from "@renderer/lib/dom"
import { cn } from "@renderer/lib/utils"
import { useAnimationControls, useDragControls } from "framer-motion"
Expand Down Expand Up @@ -148,7 +150,9 @@ export const ModalInternal: Component<{
)
const finalChildren = (
<CurrentModalContext.Provider value={ModalContextProps}>
{children ?? createElement(content, ModalProps)}
<AppErrorBoundary errorType={ErrorComponentType.Modal}>
{children ?? createElement(content, ModalProps)}
</AppErrorBoundary>
</CurrentModalContext.Provider>
)

Expand Down
61 changes: 61 additions & 0 deletions src/renderer/src/lib/dev.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export const attachOpenInEditor = (stack: string) => {
const lines = stack.split("\n")
return lines.map((line) => {
// A line like this: at App (http://localhost:5173/src/App.tsx?t=1720527056591:41:9)
// Find the `localhost` part and open the file in the editor
if (!line.includes("at ")) {
return line
}
const match = line.match(/(http:\/\/localhost:\d+\/[^:]+):(\d+):(\d+)/)

if (match) {
const [o] = match

// Find `@fs/`
// Like: `http://localhost:5173/@fs/Users/innei/git/work/rss3/follow/node_modules/.vite/deps/chunk-RPCDYKBN.js?v=757920f2:11548:26`
const realFsPath = o.split("@fs")[1]

if (realFsPath) {
return (
// Delete `v=` hash, like `v=757920f2`
<div
className="cursor-pointer"
key={line}
onClick={openInEditor.bind(
null,
realFsPath.replace(/\?v=[a-f0-9]+/, ""),
)}
>
{line}
</div>
)
} else {
// at App (http://localhost:5173/src/App.tsx?t=1720527056591:41:9)
const srcFsPath = o.split("/src")[1]

if (srcFsPath) {
const fs = srcFsPath.replace(/\?t=[a-f0-9]+/, "")

return (
<div
className="cursor-pointer"
key={line}
onClick={openInEditor.bind(
null,
`${APP_DEV_CWD}/src/renderer/src${fs}`,
)}
>
{line}
</div>
)
}
}
}

return line
})
}
// http://localhost:5173/src/App.tsx?t=1720527056591:41:9
const openInEditor = (file: string) => {
fetch(`/__open-in-editor?file=${encodeURIComponent(`${file}`)}`)
}
5 changes: 1 addition & 4 deletions src/renderer/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import ReactDOM from "react-dom/client"
import { RouterProvider } from "react-router-dom"

import { setAppIsReady } from "./atoms/app"
import { AppErrorBoundary } from "./components/common/AppErrorBoundary"
import { initializeApp } from "./initialize"
import { getOS } from "./lib/utils"
import { router } from "./router"
Expand All @@ -24,9 +23,7 @@ if (window.electron && getOS() === "Windows") {
}
ReactDOM.createRoot($container).render(
<React.StrictMode>
<AppErrorBoundary>
<RouterProvider router={router} />
</AppErrorBoundary>
<RouterProvider router={router} />
<ClickToComponent />
</React.StrictMode>,
)
Loading

0 comments on commit 2683d3a

Please sign in to comment.