import { CheckCircle, CircleSlash, Info, UserCircle } from "lucide-react"; import { useMemo, useState } from "react"; import { data } from "react-router"; 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 cn from "~/utils/cn"; import { getOSInfo, getTSVersion } from "~/utils/host-info"; import { mapNodes, sortNodeTags } from "~/utils/node-info"; import { getUserDisplayName } from "~/utils/user"; import type { Route } from "./+types/machine"; 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 }: Route.LoaderArgs) { const principal = await context.auth.require(request); if (!params.id) { throw new Error("No machine ID provided"); } if (params.id.endsWith(".ico")) { throw data(null, { status: 204 }); } let magic: string | undefined; if (context.hs.readable()) { if (context.hs.c?.dns.magic_dns) { magic = context.hs.c.dns.base_domain; } } const api = context.hsApi.getRuntimeClient( context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey), ); const [nodes, users] = await Promise.all([api.getNodes(), api.getUsers()]); const node = nodes.find((node) => node.id === params.id); const lookup = await context.agents?.lookup([node.nodeKey]); const [enhancedNode] = mapNodes([node], lookup); const tags = [...node.tags].toSorted(); const supportsNodeOwnerChange = !context.hsApi.clientHelpers.isAtleast("0.28.0-beta.1"); return { agent: context.agents?.agentID(), existingTags: sortNodeTags(nodes), magic, node: enhancedNode, stats: lookup?.[enhancedNode.nodeKey], supportsNodeOwnerChange: supportsNodeOwnerChange, tags, users, }; } export const action = machineAction; export default function Page({ loaderData: { node, tags, users, magic, agent, stats, existingTags, supportsNodeOwnerChange }, }: Route.ComponentProps) { 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 ? getUserDisplayName(node.user) : "Tag-owned"}

Status

{mapTagsToComponents(node, uiTags)} {tags.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}
  • ))}
)}
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}
  • ))}
)}
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 "—"; }