feat: add machine data

This commit is contained in:
Aarnav Tale
2024-03-25 18:47:15 -04:00
parent b38eb0885f
commit abaf71636e
10 changed files with 246 additions and 32 deletions
+47
View File
@@ -0,0 +1,47 @@
import { useToaster } from 'react-hot-toast/headless'
export default function Toaster() {
const { toasts, handlers } = useToaster()
const { startPause, endPause, calculateOffset, updateHeight } = handlers
return (
<div
className='fixed bottom-0 right-0 p-4 w-80 h-1/2 overflow-hidden'
onMouseEnter={startPause}
onMouseLeave={endPause}
>
{toasts.slice(0, 6).map(toast => {
const offset = calculateOffset(toast, {
reverseOrder: false,
gutter: -8
})
// eslint-disable-next-line @typescript-eslint/ban-types
const reference = (element: HTMLDivElement | null) => {
if (element && typeof toast.height !== 'number') {
const { height } = element.getBoundingClientRect()
updateHeight(toast.id, -height)
}
}
return (
<div
key={toast.id}
ref={reference}
className='fixed bottom-4 right-4 p-4 bg-gray-800 rounded-lg text-white transition-all duration-300'
{...toast.ariaProps}
style={{
transform: `translateY(${offset}px) translateX(${toast.visible ? 0 : 200}%)`
}}
>
{typeof toast.message === 'function' ? (
toast.message(toast)
) : (
toast.message
)}
</div>
)
})}
</div>
)
}
+2
View File
@@ -11,6 +11,7 @@ import {
} from '@remix-run/react'
import clsx from 'clsx'
import Toaster from '~/components/Toaster'
import stylesheet from '~/tailwind.css?url'
export const meta: MetaFunction = () => [
@@ -50,6 +51,7 @@ export function Layout({ children }: { readonly children: React.ReactNode }) {
</head>
<body className='overscroll-none'>
{children}
<Toaster/>
<ScrollRestoration/>
<Scripts/>
</body>
+91 -29
View File
@@ -1,32 +1,94 @@
export default function Index() {
import { ClipboardIcon } from '@heroicons/react/24/outline'
import { type LoaderFunctionArgs } from '@remix-run/node'
import { useLoaderData } from '@remix-run/react'
import clsx from 'clsx'
import { toast } from 'react-hot-toast/headless'
import { type Machine } from '~/types'
import { pull } from '~/utils/headscale'
import { getSession } from '~/utils/sessions'
import { useLiveData } from '~/utils/useLiveData'
export async function loader({ request }: LoaderFunctionArgs) {
const session = await getSession(request.headers.get('Cookie'))
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const data = await pull<{ nodes: Machine[] }>('v1/node', session.get('hsApiKey')!)
return data.nodes
}
export default function Page() {
const data = useLoaderData<typeof loader>()
useLiveData({ interval: 3000 })
return (
<div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.8' }}>
<h1>Welcome to Remix</h1>
<ul>
<li>
<a
target='_blank'
href='https://remix.run/tutorials/blog'
rel='noreferrer'
>
15m Quickstart Blog Tutorial
</a>
</li>
<li>
<a
target='_blank'
href='https://remix.run/tutorials/jokes'
rel='noreferrer'
>
Deep Dive Jokes App Tutorial
</a>
</li>
<li>
<a target='_blank' href='https://remix.run/docs' rel='noreferrer'>
Remix Docs
</a>
</li>
</ul>
</div>
<table className='table-auto w-full rounded-lg'>
<thead>
<tr className='text-left'>
<th className='pl-4'>Name</th>
<th>IP Addresses</th>
<th>Last Seen</th>
</tr>
</thead>
<tbody className='divide-y divide-zinc-200 dark:divide-zinc-700'>
{data.map(machine => (
<tr key={machine.id} className='hover:bg-zinc-100 dark:hover:bg-zinc-800'>
<td className='pt-2 pb-4 pl-4'>
<a href={`machines/${machine.id}`}>
<h1>{machine.givenName}</h1>
<span
className='text-sm font-mono text-gray-500 dark:text-gray-400'
>{machine.name}
</span
>
</a>
</td>
<td className='pt-2 pb-4 font-mono text-gray-600 dark:text-gray-300'>
{machine.ipAddresses.map((ip, index) => (
<button
key={ip}
type='button'
className='flex items-center gap-x-1 w-full'
onClick={async () => {
await navigator.clipboard.writeText(ip)
toast('Copied IP address to clipboard')
}}
>
<span className={clsx(index === 0 ? 'text-gray-600 dark:text-gray-300' : 'text-gray-400 dark:text-gray-500')}>
{ip}
</span>
<ClipboardIcon className='text-gray-400 dark:text-gray-500 w-4 h-4'/>
</button>
))}
</td>
<td>
<span
className='flex items-center gap-x-1 text-sm text-gray-500 dark:text-gray-400'
>
<svg
className={clsx(
'w-4 h-4',
machine.online
? 'text-green-700 dark:text-green-400'
: 'text-gray-300 dark:text-gray-500'
)}
viewBox='0 0 24 24'
fill='currentColor'
>
<circle cx='12' cy='12' r='8'/>
</svg>
<p>
{machine.online
? 'Connected'
: new Date(
machine.lastSeen
).toLocaleString()}
</p>
</span>
</td>
</tr>
))}
</tbody>
</table>
)
}
+28
View File
@@ -0,0 +1,28 @@
import type { User } from './User'
export type Machine = {
id: string;
machineKey: string;
nodeKey: string;
discoKey: string;
ipAddresses: string[];
name: string;
user: User;
lastSeen: Date;
expiry: Date;
preAuthKey?: unknown; // TODO
createdAt: Date;
registerMethod: 'REGISTER_METHOD_UNSPECIFIED'
| 'REGISTER_METHOD_AUTH_KEY'
| 'REGISTER_METHOD_CLI'
| 'REGISTER_METHOD_OIDC';
forcedTags: string[];
invalidTags: string[];
validTags: string[];
givenName: string;
online: boolean;
}
+13
View File
@@ -0,0 +1,13 @@
import type { Machine } from './Machine'
export type Route = {
id: string;
node: Machine;
prefix: string;
advertised: boolean;
enabled: boolean;
isPrimary: boolean;
createdAt: Date;
updatedAt: Date;
deletedAt: Date;
}
+5
View File
@@ -0,0 +1,5 @@
export type User = {
id: string;
name: string;
createdAt: Date;
}
+3
View File
@@ -0,0 +1,3 @@
export * from './Machine'
export * from './Route'
export * from './User'
+16
View File
@@ -0,0 +1,16 @@
import { useRevalidator } from '@remix-run/react'
import { useInterval } from 'usehooks-ts'
type Properties = {
interval: number;
}
export function useLiveData({ interval }: Properties) {
const revalidator = useRevalidator()
useInterval(() => {
if (revalidator.state === 'idle') {
revalidator.revalidate()
}
}, interval)
}