/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node' import { Link as RemixLink, useLoaderData } from '@remix-run/react' import { InfoIcon, GearIcon, CheckCircleIcon, SkipIcon, PersonIcon } from '@primer/octicons-react' import { useMemo, useState } from 'react' import Attribute from '~/components/Attribute' import Button from '~/components/Button' import Card from '~/components/Card' import Menu from '~/components/Menu' import Tooltip from '~/components/Tooltip' import StatusCircle from '~/components/StatusCircle' import { Machine, Route, User } 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 Link from '~/components/Link' import { menuAction } from '../_data.machines._index/action' import MenuOptions from '../_data.machines._index/menu' import Routes from '../_data.machines._index/dialogs/routes' 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') } const context = await loadContext() let magic: string | undefined if (context.config.read) { const config = await loadConfig() if (config.dns.magic_dns) { magic = config.dns.base_domain } } const [machine, routes, users] = await Promise.all([ pull<{ node: Machine }>(`v1/node/${params.id}`, session.get('hsApiKey')!), pull<{ routes: Route[] }>('v1/routes', session.get('hsApiKey')!), pull<{ users: User[] }>('v1/user', session.get('hsApiKey')!), ]) return { machine: machine.node, routes: routes.routes.filter(route => route.node.id === params.id), users: users.users, magic, } } export async function action({ request }: ActionFunctionArgs) { return menuAction(request) } export default function Page() { const { machine, magic, routes, users } = useLoaderData() const routesState = useState(false) useLiveData({ interval: 1000 }) const expired = machine.expiry === '0001-01-01 00:00:00' || machine.expiry === '0001-01-01T00:00:00Z' || machine.expiry === null ? false : new Date(machine.expiry).getTime() < Date.now() const tags = [ ...machine.forcedTags, ...machine.validTags, ] if (expired) { tags.unshift('Expired') } // This is much easier with Object.groupBy but it's too new for us const { exit, subnet, subnetApproved } = routes.reduce((acc, route) => { if (route.prefix === '::/0' || route.prefix === '0.0.0.0/0') { acc.exit.push(route) return acc } if (route.enabled) { acc.subnetApproved.push(route) return acc } acc.subnet.push(route) return acc }, { exit: [], subnetApproved: [], subnet: [] }) const exitEnabled = useMemo(() => { if (exit.length !== 2) return false return exit[0].enabled && exit[1].enabled }, [exit]) if (exitEnabled) { tags.unshift('Exit Node') } if (subnetApproved.length > 0) { tags.unshift('Subnets') } return (

All Machines / {machine.givenName}

{machine.givenName}

Machine Settings } />
Managed by By default, a machine’s permissions match its creator’s.
{machine.user.name}

Status

{tags.map(tag => ( {tag} ))}

Subnets & Routing

Subnets let you expose physical network routes onto Tailscale. {' '} Learn More

Approved Traffic to these routes are being routed through this machine.
{subnetApproved.length === 0 ? ( ) : (
    {subnetApproved.map(route => (
  • {route.prefix}
  • ))}
)}
Awaiting Approval This machine is advertising these routes, but they must be approved before traffic will be routed to them.
{subnet.length === 0 ? ( ) : (
    {subnet.map(route => (
  • {route.prefix}
  • ))}
)}
Exit Node Whether this machine can act as an exit node for your tailnet.
{exit.length === 0 ? ( ) : exitEnabled ? ( Allowed ) : ( Awaiting Approval )}

Machine Details

{magic ? ( ) : undefined}
) }