feat: cleanup header and extract to component

This commit is contained in:
Aarnav Tale 2024-05-04 15:04:22 -04:00
parent c6cdcf35eb
commit 5ff09e44d9
No known key found for this signature in database
5 changed files with 119 additions and 75 deletions

109
app/components/Header.tsx Normal file
View File

@ -0,0 +1,109 @@
import { GearIcon, GlobeIcon, LockIcon, PaperAirplaneIcon, PeopleIcon, PersonIcon, ServerIcon } from '@primer/octicons-react'
import { Form } from '@remix-run/react'
import { cn } from '~/utils/cn'
import { type Context } from '~/utils/config'
import { type SessionData } from '~/utils/sessions'
import Menu from './Menu'
import TabLink from './TabLink'
type Properties = {
readonly data?: Context & { user?: SessionData['user'] };
}
type LinkProperties = {
readonly href: string;
readonly text: string;
readonly isMenu?: boolean;
}
function Link({ href, text, isMenu }: LinkProperties) {
return (
<a
href={href}
target='_blank'
rel='noreferrer'
className={cn(
!isMenu && 'text-ui-300 hover:text-ui-50 hover:underline hidden sm:block'
)}
>
{text}
</a>
)
}
export default function Header({ data }: Properties) {
return (
<header className='bg-main-700 dark:bg-main-800 text-ui-50'>
<div className='container flex items-center justify-between py-4'>
<div className='flex items-center gap-x-2'>
<PaperAirplaneIcon className='w-6 h-6'/>
<h1 className='text-2xl'>Headplane</h1>
</div>
<div className='flex items-center gap-x-4'>
<Link href='https://tailscale.com/download' text='Download'/>
<Link href='https://github.com/tale/headplane' text='GitHub'/>
<Link href='https://github.com/juanfont/headscale' text='Headscale'/>
{data?.user ? (
<Menu>
<Menu.Button className={cn(
'rounded-full h-9 w-9',
'border border-main-600 dark:border-main-700',
'hover:bg-main-600 dark:hover:bg-main-700'
)}
>
<PersonIcon className='h-5 w-5 mt-0.5'/>
</Menu.Button>
<Menu.Items>
<Menu.Item className='text-right'>
<p className='font-bold'>{data.user.name}</p>
<p>{data.user.email}</p>
</Menu.Item>
<Menu.Item className='text-right sm:hidden'>
<Link
isMenu
href='https://tailscale.com/download'
text='Download'
/>
</Menu.Item>
<Menu.Item className='text-right sm:hidden'>
<Link
isMenu
href='https://github.com/tale/headplane'
text='GitHub'
/>
</Menu.Item>
<Menu.Item className='text-right sm:hidden'>
<Link
isMenu
href='https://github.com/juanfont/headscale'
text='Headscale'
/>
</Menu.Item>
<Menu.Item className='text-red-500 dark:text-red-400'>
<Form method='POST' action='/logout'>
<button type='submit' className='w-full text-right'>
Logout
</button>
</Form>
</Menu.Item>
</Menu.Items>
</Menu>
) : undefined}
</div>
</div>
<nav className='container flex items-center gap-x-4 overflow-x-auto'>
<TabLink to='/machines' name='Machines' icon={<ServerIcon className='w-4 h-4'/>}/>
<TabLink to='/users' name='Users' icon={<PeopleIcon className='w-4 h-4'/>}/>
{data?.hasAcl ? <TabLink to='/acls' name='Access Control' icon={<LockIcon className='w-4 h-4'/>}/> : undefined}
{data?.hasConfig ? (
<>
<TabLink to='/dns' name='DNS' icon={<GlobeIcon className='w-4 h-4'/>}/>
<TabLink to='/settings' name='Settings' icon={<GearIcon className='w-4 h-4'/>}/>
</>
) : undefined}
</nav>
</header>
)
}

View File

@ -13,8 +13,9 @@ export default function TabLink({ name, to, icon }: Properties) {
<NavLink
to={to}
className={({ isActive, isPending }) => clsx(
'flex items-center gap-x-2 p-2 border-b-2 text-md',
isActive ? 'border-white' : 'border-transparent'
'flex items-center gap-x-2 p-2 border-b-2 text-md text-nowrap',
isActive ? 'border-white' : 'border-transparent',
isPending && 'animate-pulse'
)}
>
{icon} {name}

View File

@ -1,10 +1,8 @@
import { Cog8ToothIcon, CpuChipIcon, GlobeAltIcon, LockClosedIcon, ServerStackIcon, UserCircleIcon, UsersIcon } from '@heroicons/react/24/outline'
import { type LoaderFunctionArgs, redirect } from '@remix-run/node'
import { Form, Outlet, useLoaderData } from '@remix-run/react'
import { Outlet, useLoaderData } from '@remix-run/react'
import { ErrorPopup } from '~/components/Error'
import Menu from '~/components/Menu'
import TabLink from '~/components/TabLink'
import Header from '~/components/Header'
import { getContext } from '~/utils/config'
import { HeadscaleError, pull } from '~/utils/headscale'
import { destroySession, getSession } from '~/utils/sessions'
@ -20,7 +18,6 @@ export async function loader({ request }: LoaderFunctionArgs) {
await pull('v1/apikey', session.get('hsApiKey')!)
} catch (error) {
if (error instanceof HeadscaleError) {
console.error(error)
// Safest to just redirect to login if we can't pull
return redirect('/login', {
headers: {
@ -46,58 +43,9 @@ export default function Layout() {
return (
<>
<header className='mb-6 bg-main-700 dark:bg-main-800 text-white'>
<nav className='container mx-auto'>
<div className='flex items-center justify-between mb-8 pt-4'>
<div className='flex items-center gap-x-2'>
<CpuChipIcon className='w-8 h-8'/>
<h1 className='text-2xl'>Headplane</h1>
</div>
<div className='flex items-center gap-x-4'>
<a href='https://tailscale.com/download' target='_blank' rel='noreferrer' className='text-gray-300 hover:text-white'>
Download
</a>
<a href='https://github.com/tale/headplane' target='_blank' rel='noreferrer' className='text-gray-300 hover:text-white'>
GitHub
</a>
<a href='https://github.com/juanfont/headscale' target='_blank' rel='noreferrer' className='text-gray-300 hover:text-white'>
Headscale
</a>
<Menu>
<Menu.Button>
<UserCircleIcon className='w-8 h-8'/>
</Menu.Button>
<Menu.Items>
<Menu.Item className='text-right'>
<p className='font-bold'>{data.user?.name}</p>
<p>{data.user?.email}</p>
</Menu.Item>
<Menu.Item className='text-red-500 dark:text-red-400'>
<Form method='POST' action='/logout'>
<button type='submit' className='w-full text-right'>
Logout
</button>
</Form>
</Menu.Item>
</Menu.Items>
</Menu>
</div>
</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'/>}/>
{data.hasAcl ? <TabLink to='/acls' name='Access Control' icon={<LockClosedIcon className='w-5 h-5'/>}/> : undefined}
{data.hasConfig ? (
<>
<TabLink to='/dns' name='DNS' icon={<GlobeAltIcon className='w-5 h-5'/>}/>
<TabLink to='/settings' name='Settings' icon={<Cog8ToothIcon className='w-5 h-5'/>}/>
</>
) : undefined}
</div>
</nav>
</header>
<Header data={data}/>
<main className='container mx-auto overscroll-contain mb-24'>
<main className='container mx-auto overscroll-contain mt-4 mb-24'>
<Outlet/>
</main>
</>
@ -107,21 +55,7 @@ export default function Layout() {
export function ErrorBoundary() {
return (
<>
<header className='mb-16 bg-main-700 dark:bg-main-800 text-white'>
<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>
<Header/>
<ErrorPopup type='embedded'/>
</>
)

View File

@ -196,7 +196,7 @@ export function registerConfigWatcher() {
})
}
type Context = {
export type Context = {
hasDockerSock: boolean;
hasConfig: boolean;
hasConfigWrite: boolean;

View File

@ -1,6 +1,6 @@
import { createCookieSessionStorage } from '@remix-run/node' // Or cloudflare/deno
type SessionData = {
export type SessionData = {
hsApiKey: string;
authState: string;
authNonce: string;