mirror of
https://github.com/tale/headplane.git
synced 2026-08-27 15:37:04 +00:00
feat: add proper error component
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
import { type ReactNode } from 'react'
|
import clsx from 'clsx'
|
||||||
|
import { type HTMLProps } from 'react'
|
||||||
|
|
||||||
export default function Code({ children }: { readonly children: ReactNode }) {
|
type Properties = HTMLProps<HTMLSpanElement>
|
||||||
|
|
||||||
|
export default function Code(properties: Properties) {
|
||||||
return (
|
return (
|
||||||
<code className='bg-gray-100 dark:bg-zinc-700 p-0.5 rounded-md'>
|
<code className={clsx('bg-gray-100 dark:bg-zinc-700 p-0.5 rounded-md', properties.className)}>
|
||||||
{children}
|
{properties.children}
|
||||||
</code>
|
</code>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { Transition } from '@headlessui/react'
|
||||||
|
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { isRouteErrorResponse, useRouteError } from '@remix-run/react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { Fragment, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import Code from './Code'
|
||||||
|
|
||||||
|
type Properties = {
|
||||||
|
readonly type?: 'full' | 'embedded';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorPopup({ type = 'full' }: Properties) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const error = useRouteError()
|
||||||
|
const routing = isRouteErrorResponse(error)
|
||||||
|
const message = (error instanceof Error ? error.message : 'An unexpected error occurred')
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
// Debounce the error modal so it doesn't show up for a split second
|
||||||
|
// when the user navigates to a new page.
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsOpen(true)
|
||||||
|
}, 150)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition as={Fragment} show={isOpen}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter='ease-out duration-150'
|
||||||
|
enterFrom='opacity-0 scale-95'
|
||||||
|
enterTo='opacity-100 scale-100'
|
||||||
|
>
|
||||||
|
|
||||||
|
<div className={clsx(
|
||||||
|
'flex items-center justify-center overflow-clip',
|
||||||
|
type === 'full' ? 'min-h-screen' : 'mt-24'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={clsx(
|
||||||
|
'flex flex-col items-center justify-center space-y-2 w-full sm:w-1/2 xl:w-1/3',
|
||||||
|
'bg-white dark:bg-zinc-800 rounded-lg py-8 px-4 md:px-16',
|
||||||
|
'border border-gray-200 dark:border-zinc-700 text-center'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ExclamationTriangleIcon className='w-12 h-12 text-red-500'/>
|
||||||
|
<h1 className='text-2xl font-semibold text-gray-800 dark:text-gray-100'>
|
||||||
|
{routing ? error.status : 'Error'}
|
||||||
|
</h1>
|
||||||
|
{routing ? (
|
||||||
|
<p className='text-gray-500 dark:text-gray-400'>
|
||||||
|
{error.statusText}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<Code className='text-sm'>
|
||||||
|
{message}
|
||||||
|
</Code>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition.Child>
|
||||||
|
</Transition>
|
||||||
|
)
|
||||||
|
}
|
||||||
+3
-37
@@ -1,17 +1,13 @@
|
|||||||
import { ExclamationTriangleIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/outline'
|
|
||||||
import type { LinksFunction, MetaFunction } from '@remix-run/node'
|
import type { LinksFunction, MetaFunction } from '@remix-run/node'
|
||||||
import {
|
import {
|
||||||
isRouteErrorResponse,
|
|
||||||
Links,
|
Links,
|
||||||
Meta,
|
Meta,
|
||||||
Outlet,
|
Outlet,
|
||||||
Scripts,
|
Scripts,
|
||||||
ScrollRestoration,
|
ScrollRestoration
|
||||||
useRouteError
|
|
||||||
} from '@remix-run/react'
|
} from '@remix-run/react'
|
||||||
import clsx from 'clsx'
|
|
||||||
|
|
||||||
import Code from '~/components/Code'
|
import { ErrorPopup } from '~/components/Error'
|
||||||
import Toaster from '~/components/Toaster'
|
import Toaster from '~/components/Toaster'
|
||||||
import stylesheet from '~/tailwind.css?url'
|
import stylesheet from '~/tailwind.css?url'
|
||||||
import { getContext } from '~/utils/config'
|
import { getContext } from '~/utils/config'
|
||||||
@@ -68,37 +64,7 @@ export function Layout({ children }: { readonly children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBoundary() {
|
export function ErrorBoundary() {
|
||||||
const error = useRouteError()
|
return <ErrorPopup/>
|
||||||
const routing = isRouteErrorResponse(error)
|
|
||||||
const message = (error instanceof Error ? error.message : 'An unexpected error occurred')
|
|
||||||
return (
|
|
||||||
<div className='flex min-h-screen items-center justify-center'>
|
|
||||||
<div className={clsx(
|
|
||||||
'w-1/3 border p-4 rounded-lg flex flex-col items-center text-center',
|
|
||||||
routing ? 'gap-2' : 'gap-4'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{routing ? (
|
|
||||||
<>
|
|
||||||
<QuestionMarkCircleIcon className='text-gray-500 w-14 h-14'/>
|
|
||||||
<h1 className='text-2xl font-bold'>{error.status}</h1>
|
|
||||||
<p className='opacity-50 text-sm'>{error.statusText}</p>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ExclamationTriangleIcon className='text-red-500 w-14 h-14'/>
|
|
||||||
<h1 className='text-2xl font-bold'>Error</h1>
|
|
||||||
<Code>
|
|
||||||
{message}
|
|
||||||
</Code>
|
|
||||||
<p className='opacity-50 text-sm mt-4'>
|
|
||||||
If you are the administrator of this site, please check your logs for information.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
|||||||
+24
-1
@@ -1,7 +1,8 @@
|
|||||||
import { Cog8ToothIcon, CpuChipIcon, GlobeAltIcon, LockClosedIcon, ServerStackIcon, UsersIcon } from '@heroicons/react/24/outline'
|
import { Cog8ToothIcon, CpuChipIcon, GlobeAltIcon, LockClosedIcon, ServerStackIcon, UsersIcon } from '@heroicons/react/24/outline'
|
||||||
import { type LoaderFunctionArgs, redirect } from '@remix-run/node'
|
import { type LoaderFunctionArgs, redirect } from '@remix-run/node'
|
||||||
import { Outlet } from '@remix-run/react'
|
import { Outlet, useRouteError } from '@remix-run/react'
|
||||||
|
|
||||||
|
import { ErrorPopup } from '~/components/Error'
|
||||||
import TabLink from '~/components/TabLink'
|
import TabLink from '~/components/TabLink'
|
||||||
import { HeadscaleError, pull } from '~/utils/headscale'
|
import { HeadscaleError, pull } from '~/utils/headscale'
|
||||||
import { destroySession, getSession } from '~/utils/sessions'
|
import { destroySession, getSession } from '~/utils/sessions'
|
||||||
@@ -61,3 +62,25 @@ export default function Layout() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ErrorBoundary() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header className='mb-16 bg-gray-800 text-white dark:bg-gray-700'>
|
||||||
|
<nav className='container mx-auto'>
|
||||||
|
<div className='flex items-center gap-x-2 mb-8 pt-4'>
|
||||||
|
<CpuChipIcon className='w-8 h-8'/>
|
||||||
|
<h1 className='text-2xl'>Headplane</h1>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-x-4'>
|
||||||
|
<TabLink to='/machines' name='Machines' icon={<ServerStackIcon className='w-5 h-5'/>}/>
|
||||||
|
<TabLink to='/users' name='Users' icon={<UsersIcon className='w-5 h-5'/>}/>
|
||||||
|
<TabLink to='/acls' name='Access Control' icon={<LockClosedIcon className='w-5 h-5'/>}/>
|
||||||
|
<TabLink to='/dns' name='DNS' icon={<GlobeAltIcon className='w-5 h-5'/>}/>
|
||||||
|
<TabLink to='/settings' name='Settings' icon={<Cog8ToothIcon className='w-5 h-5'/>}/>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<ErrorPopup type='embedded'/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user