feat: add functional machine overview page and fix types

This commit is contained in:
Aarnav Tale 2024-06-02 01:33:40 -04:00
parent c08203cc76
commit 3ffbabd7fc
No known key found for this signature in database
12 changed files with 458 additions and 289 deletions

View file

@ -4,72 +4,183 @@ import { Link, useLoaderData } from '@remix-run/react'
import Attribute from '~/components/Attribute'
import Card from '~/components/Card'
import StatusCircle from '~/components/StatusCircle'
import { type Machine } from '~/types'
import { type Machine, Route } from '~/types'
import { cn } from '~/utils/cn'
import { loadContext } from '~/utils/config/headplane'
import { loadConfig } from '~/utils/config/headscale'
import { pull } from '~/utils/headscale'
import { getSession } from '~/utils/sessions'
import { useLiveData } from '~/utils/useLiveData'
import MenuOptions from './_data.machines._index/menu'
export async function loader({ request, params }: LoaderFunctionArgs) {
const session = await getSession(request.headers.get('Cookie'))
if (!params.id) {
throw new Error('No machine ID provided')
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const data = await pull<{ node: Machine }>(`v1/node/${params.id}`, session.get('hsApiKey')!)
return data.node
const context = await loadContext()
let magic: string | undefined
if (context.config.read) {
const config = await loadConfig()
if (config.dns_config.magic_dns) {
magic = config.dns_config.base_domain
}
}
const [machine, routes] = await Promise.all([
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
pull<{ node: Machine }>(`v1/node/${params.id}`, session.get('hsApiKey')!),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
pull<{ routes: Route[] }>('v1/routes', session.get('hsApiKey')!),
])
return {
machine: machine.node,
routes: routes.routes.filter(route => route.node.id === params.id),
magic,
}
}
export default function Page() {
const data = useLoaderData<typeof loader>()
const { machine, magic, routes } = useLoaderData<typeof loader>()
useLiveData({ interval: 1000 })
const expired = new Date(machine.expiry).getTime() < Date.now()
const tags = [
...machine.forcedTags,
...machine.validTags,
]
if (expired) {
tags.unshift('Expired')
}
return (
<div>
<p className='mb-4 text-gray-500 dark:text-gray-400 text-sm'>
<p className="mb-8 text-md">
<Link
to='/machines'
className='font-bold text-gray-700 dark:text-gray-300 hover:underline'
to="/machines"
className="font-medium"
>
All Machines
</Link>
{' / '}
{data.givenName}
<span className="mx-2">
/
</span>
{machine.givenName}
</p>
<span className='flex items-baseline gap-x-4 text-sm mb-4'>
<h1 className='text-2xl font-bold'>
{data.givenName}
</h1>
<StatusCircle isOnline={data.online} className='w-4 h-4'/>
</span>
<Card variant='flat'>
<Attribute name='Creator' value={data.user.name}/>
<Attribute name='Node ID' value={data.id}/>
<Attribute name='Node Name' value={data.givenName}/>
<Attribute name='Hostname' value={data.name}/>
<div className="flex justify-between items-center">
<span className="flex items-baseline gap-x-4 text-sm mb-4">
<h1 className="text-2xl font-medium">
{machine.givenName}
</h1>
<StatusCircle isOnline={machine.online} className="w-4 h-4" />
</span>
<MenuOptions
machine={machine}
routes={routes}
magic={magic}
/>
</div>
<div className="flex gap-1 mt-1 mb-8">
{tags.map(tag => (
<span
key={tag}
className={cn(
'text-xs rounded-md px-1.5 py-0.5',
'bg-ui-200 dark:bg-ui-800',
'text-ui-600 dark:text-ui-300',
)}
>
{tag}
</span>
))}
</div>
<h2 className="text-xl font-medium mb-4">
Machine Details
</h2>
<Card variant="flat" className="w-full max-w-full">
<Attribute name="Creator" value={machine.user.name} />
<Attribute name="Node ID" value={machine.id} />
<Attribute name="Node Name" value={machine.givenName} />
<Attribute name="Hostname" value={machine.name} />
<Attribute
isCopyable
name='Node Key'
value={data.nodeKey}
name="Node Key"
value={machine.nodeKey}
/>
<Attribute
name='Created'
value={new Date(data.createdAt).toLocaleString()}
name="Created"
value={new Date(machine.createdAt).toLocaleString()}
/>
<Attribute
name='Last Seen'
value={new Date(data.lastSeen).toLocaleString()}
name="Last Seen"
value={new Date(machine.lastSeen).toLocaleString()}
/>
<Attribute
name='Expiry'
value={new Date(data.expiry).toLocaleString()}
name="Expiry"
value={new Date(machine.expiry).toLocaleString()}
/>
<Attribute
isCopyable
name='Domain'
value={`${data.givenName}.${data.user.name}.ts.net`}
name="Domain"
value={`${machine.givenName}.${machine.user.name}.ts.net`}
/>
</Card>
<h2 className="text-xl font-medium mb-4 mt-8">
Machine Routes
</h2>
<Card variant="flat" className="w-full max-w-full">
{routes.length === 0
? (
<div
className={cn(
'flex py-4 px-4',
'items-center justify-center',
'text-ui-600 dark:text-ui-300',
)}
>
<p>
No routes are advertised on this machine.
</p>
</div>
)
: [...routes, ...routes].map((route, i) => (
<div
key={route.node.id}
className={cn(
'flex items-center justify-between',
routes.length - 1 === i ? 'border-b pb-3 mb-2' : '',
'border-ui-100 dark:border-ui-800',
)}
>
<div>
<p className="font-mono mb-1">
{route.prefix}
</p>
<p className="text-sm text-ui-600 dark:text-ui-300">
{' '}
(Created:
{' '}
{new Date(route.createdAt).toLocaleString()}
)
</p>
</div>
<div className="text-right">
<p className="mb-1">
{route.enabled ? 'Enabled' : 'Disabled'}
</p>
<p className="text-sm text-ui-600 dark:text-ui-300">
{route.isPrimary ? 'Primary' : 'Secondary'}
</p>
</div>
</div>
))}
</Card>
</div>
)
}