import { CheckCircle, CircleSlash, Info, UserCircle } from 'lucide-react'; import { useMemo, useState } from 'react'; import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router'; import { Link as RemixLink, useLoaderData } from 'react-router'; import { mapTag } from 'yaml/util'; import Attribute from '~/components/Attribute'; import Button from '~/components/Button'; import Card from '~/components/Card'; import Chip from '~/components/Chip'; import Link from '~/components/Link'; import StatusCircle from '~/components/StatusCircle'; import Tooltip from '~/components/Tooltip'; import type { LoadContext } from '~/server'; import type { Machine, Route, User } from '~/types'; import cn from '~/utils/cn'; import { getOSInfo, getTSVersion } from '~/utils/host-info'; import { mapNodes } from '~/utils/node-info'; import { mapTagsToComponents, uiTagsForNode } from './components/machine-row'; import MenuOptions from './components/menu'; import Routes from './dialogs/routes'; import { machineAction } from './machine-actions'; export async function loader({ request, params, context, }: LoaderFunctionArgs) { const session = await context.sessions.auth(request); if (!params.id) { throw new Error('No machine ID provided'); } let magic: string | undefined; if (context.hs.readable()) { if (context.hs.c?.dns.magic_dns) { magic = context.hs.c.dns.base_domain; } } const [machine, { routes }, { users }] = await Promise.all([ context.client.get<{ node: Machine }>( `v1/node/${params.id}`, session.get('api_key')!, ), context.client.get<{ routes: Route[] }>( 'v1/routes', session.get('api_key')!, ), context.client.get<{ users: User[] }>('v1/user', session.get('api_key')!), ]); const [node] = mapNodes([machine.node], routes); const lookup = await context.agents?.lookup([node.nodeKey]); return { node, users, magic, agent: context.agents?.agentID(), stats: lookup?.[node.nodeKey], }; } export async function action(request: ActionFunctionArgs) { return machineAction(request); } export default function Page() { const { node, magic, users, agent, stats } = useLoaderData(); const [showRouting, setShowRouting] = useState(false); const uiTags = useMemo(() => { const tags = uiTagsForNode(node, agent === node.nodeKey); return tags; }, [node, agent]); return (

All Machines / {node.givenName}

{node.givenName}

Managed by By default, a machine’s permissions match its creator’s.
{node.user.name}

Status

{mapTagsToComponents(node, uiTags)} {node.validTags.map((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.
{node.customRouting.subnetApprovedRoutes.length === 0 ? ( ) : (
    {node.customRouting.subnetApprovedRoutes.map((route) => (
  • {route.prefix}
  • ))}
)}
Awaiting Approval This machine is advertising these routes, but they must be approved before traffic will be routed to them.
{node.customRouting.subnetWaitingRoutes.length === 0 ? ( ) : (
    {node.customRouting.subnetWaitingRoutes.map((route) => (
  • {route.prefix}
  • ))}
)}
Exit Node Whether this machine can act as an exit node for your tailnet.
{node.customRouting.exitRoutes.length === 0 ? ( ) : node.customRouting.exitApproved ? ( Allowed ) : ( Awaiting Approval )}

Machine Details

Information about this machine’s network. Used to debug connection issues.

{stats ? ( <> ) : undefined} {magic ? ( ) : undefined}

Addresses

{magic ? ( ) : undefined} {stats ? ( <>

Client Connectivity

) : undefined}
); } function getIpv4Address(addresses: string[]) { for (const address of addresses) { if (address.startsWith('100.')) { // Return the first CGNAT address return address; } } return '—'; } function getIpv6Address(addresses: string[]) { for (const address of addresses) { if (address.startsWith('fd')) { // Return the first IPv6 address return address; } } return '—'; }