import { ChevronDown, Copy } from "lucide-react"; import { useMemo } from "react"; import Chip from "~/components/Chip"; import Link from "~/components/link"; import { Menu, MenuContent, MenuItem, MenuTrigger } from "~/components/menu"; import StatusCircle from "~/components/StatusCircle"; import { ExitNodeTag } from "~/components/tags/ExitNode"; import { ExpiryTag } from "~/components/tags/Expiry"; import { HeadplaneAgentTag } from "~/components/tags/HeadplaneAgent"; import { SubnetTag } from "~/components/tags/Subnet"; import { TailscaleSSHTag } from "~/components/tags/TailscaleSSH"; import type { User } from "~/types"; import cn from "~/utils/cn"; import * as hinfo from "~/utils/host-info"; import type { PopulatedNode } from "~/utils/node-info"; import { formatTimeDelta } from "~/utils/time"; import toast from "~/utils/toast"; import { getUserDisplayName } from "~/utils/user"; import MenuOptions from "./menu"; interface Props { node: PopulatedNode; users: User[]; isAgent?: boolean; magic?: string; isDisabled?: boolean; existingTags?: string[]; supportsNodeOwnerChange: boolean; } export default function MachineRow({ node, users, isAgent, magic, isDisabled, existingTags, supportsNodeOwnerChange, }: Props) { const uiTags = useMemo(() => uiTagsForNode(node, isAgent), [node, isAgent]); const ipOptions = useMemo(() => { if (magic) { return [...node.ipAddresses, `${node.givenName}.${magic}`]; } return node.ipAddresses; }, [magic, node.ipAddresses]); return (

{node.givenName}

{node.user ? getUserDisplayName(node.user) : "Tag-owned"}

{mapTagsToComponents(node, uiTags)} {node.tags?.map((tag) => ( ))}
{node.ipAddresses[0]} {ipOptions.map((ip) => ( { await navigator.clipboard.writeText(ip); toast("Copied IP address to clipboard"); }} >
{ip}
))}
{/* We pass undefined when agents are not enabled */} {isAgent !== undefined ? ( {node.hostInfo !== undefined ? ( <>

{hinfo.getTSVersion(node.hostInfo)}

{hinfo.getOSInfo(node.hostInfo)}

) : (

Unknown

)} ) : undefined}

{node.online && !node.expired ? "Connected" : new Date(node.lastSeen).toLocaleString()}

{!(node.online && !node.expired) && (

{formatTimeDelta(new Date(node.lastSeen))}

)}
); } export function uiTagsForNode(node: PopulatedNode, isAgent?: boolean) { const uiTags: string[] = []; if (node.expired) { uiTags.push("expired"); } if (node.expiry === null) { uiTags.push("no-expiry"); } if (node.customRouting.exitRoutes.length > 0) { if (node.customRouting.exitApproved) { uiTags.push("exit-approved"); } else { uiTags.push("exit-waiting"); } } if (node.customRouting.subnetWaitingRoutes.length > 0) { uiTags.push("subnet-waiting"); } else if (node.customRouting.subnetApprovedRoutes.length > 0) { uiTags.push("subnet-approved"); } if (node.hostInfo?.sshHostKeys && node.hostInfo?.sshHostKeys.length > 0) { uiTags.push("tailscale-ssh"); } if (isAgent === true) { uiTags.push("headplane-agent"); } return uiTags; } export function mapTagsToComponents(node: PopulatedNode, uiTags: string[]) { return uiTags.map((tag) => { switch (tag) { case "exit-approved": case "exit-waiting": { return ; } case "subnet-approved": case "subnet-waiting": { return ; } case "expired": case "no-expiry": { return ; } case "tailscale-ssh": { return ; } case "headplane-agent": { return ; } default: { return null; } } }); }