headplane/app/routes/machines/components/machine-row.tsx

214 lines
5.1 KiB
TypeScript
Raw Normal View History

2024-12-31 10:30:14 +05:30
import { ChevronDownIcon, CopyIcon } from '@primer/octicons-react';
import { useMemo } from 'react';
2025-01-23 11:16:56 -05:00
import { Link } from 'react-router';
import Chip from '~/components/Chip';
2024-12-31 10:30:14 +05:30
import Menu from '~/components/Menu';
import StatusCircle from '~/components/StatusCircle';
2025-04-18 14:40:56 -04:00
import type { User } from '~/types';
2025-01-28 17:46:16 -05:00
import cn from '~/utils/cn';
2025-01-08 14:33:16 +05:30
import * as hinfo from '~/utils/host-info';
2025-04-18 14:40:56 -04:00
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 { PopulatedNode } from '~/utils/node-info';
2025-01-28 16:18:51 -05:00
import toast from '~/utils/toast';
2024-12-31 10:30:14 +05:30
import MenuOptions from './menu';
interface Props {
2025-04-18 14:40:56 -04:00
node: PopulatedNode;
2025-01-08 14:33:16 +05:30
users: User[];
isAgent?: boolean;
2025-01-08 14:33:16 +05:30
magic?: string;
isDisabled?: boolean;
}
2025-01-23 11:16:56 -05:00
export default function MachineRow({
2025-04-18 14:40:56 -04:00
node,
2025-01-23 11:16:56 -05:00
users,
isAgent,
magic,
isDisabled,
2025-01-23 11:16:56 -05:00
}: Props) {
2025-04-18 14:40:56 -04:00
const uiTags = useMemo(() => {
const tags = uiTagsForNode(node, isAgent);
return tags;
}, [node, isAgent]);
2025-01-28 16:02:47 -05:00
const ipOptions = useMemo(() => {
if (magic) {
2025-04-18 14:40:56 -04:00
return [...node.ipAddresses, `${node.givenName}.${magic}`];
2025-01-28 16:02:47 -05:00
}
2025-04-18 14:40:56 -04:00
return node.ipAddresses;
}, [magic, node.ipAddresses]);
2025-01-28 16:02:47 -05:00
return (
<tr
2025-04-18 14:40:56 -04:00
key={node.id}
2025-04-01 12:27:44 -04:00
className="group hover:bg-headplane-50 dark:hover:bg-headplane-950"
>
2025-05-29 09:45:47 -04:00
<td className="pl-0.5 py-2 focus-within:ring-3">
<Link
2025-04-18 14:40:56 -04:00
to={`/machines/${node.id}`}
2025-05-29 09:45:47 -04:00
className={cn('group/link h-full focus:outline-hidden')}
>
2024-12-31 10:30:14 +05:30
<p
className={cn(
'font-semibold leading-snug',
'group-hover/link:text-blue-600',
2025-05-29 09:45:47 -04:00
'dark:group-hover/link:text-blue-400',
2024-12-31 10:30:14 +05:30
)}
>
2025-04-18 14:40:56 -04:00
{node.givenName}
</p>
2025-04-18 14:45:22 -04:00
<p className="text-sm opacity-50">{node.user.name}</p>
2025-04-22 09:36:14 -04:00
<div className="flex gap-1 flex-wrap mt-1.5">
2025-04-18 14:40:56 -04:00
{mapTagsToComponents(node, uiTags)}
{node.validTags.map((tag) => (
<Chip key={tag} text={tag} />
))}
</div>
</Link>
</td>
<td className="py-2">
<div className="flex items-center gap-x-1">
2025-04-18 14:40:56 -04:00
{node.ipAddresses[0]}
2025-01-28 16:02:47 -05:00
<Menu placement="bottom end">
2025-01-23 11:16:56 -05:00
<Menu.IconButton className="bg-transparent" label="IP Addresses">
<ChevronDownIcon className="w-4 h-4" />
</Menu.IconButton>
2025-01-28 16:02:47 -05:00
<Menu.Panel
onAction={async (key) => {
await navigator.clipboard.writeText(key.toString());
toast('Copied IP address to clipboard');
}}
>
<Menu.Section>
{ipOptions.map((ip) => (
<Menu.Item key={ip} textValue={ip}>
<div
className={cn(
'flex items-center justify-between',
'text-sm w-full gap-x-6',
)}
>
{ip}
<CopyIcon className="w-3 h-3" />
</div>
</Menu.Item>
))}
</Menu.Section>
</Menu.Panel>
</Menu>
</div>
</td>
{/* We pass undefined when agents are not enabled */}
{isAgent !== undefined ? (
<td className="py-2">
2025-04-18 14:40:56 -04:00
{node.hostInfo !== undefined ? (
<>
2025-04-18 14:40:56 -04:00
<p className="leading-snug">
{hinfo.getTSVersion(node.hostInfo)}
</p>
<p className="text-sm opacity-50 max-w-48 truncate">
2025-04-18 14:40:56 -04:00
{hinfo.getOSInfo(node.hostInfo)}
</p>
</>
) : (
<p className="text-sm opacity-50">Unknown</p>
)}
</td>
) : undefined}
<td className="py-2">
2024-12-31 10:30:14 +05:30
<span
className={cn(
'flex items-center gap-x-1 text-sm',
2025-02-04 17:21:03 -05:00
'text-headplane-600 dark:text-headplane-300',
2024-12-31 10:30:14 +05:30
)}
>
2024-05-01 02:47:45 -04:00
<StatusCircle
2025-04-18 14:40:56 -04:00
isOnline={node.online && !node.expired}
className="w-4 h-4"
2024-05-01 02:47:45 -04:00
/>
<p suppressHydrationWarning>
2025-04-18 14:40:56 -04:00
{node.online && !node.expired
? 'Connected'
2025-04-18 14:40:56 -04:00
: new Date(node.lastSeen).toLocaleString()}
</p>
</span>
</td>
<td className="py-2 pr-0.5">
<MenuOptions
2025-04-18 14:40:56 -04:00
node={node}
users={users}
magic={magic}
isDisabled={isDisabled}
2024-05-01 02:47:45 -04:00
/>
</td>
</tr>
2024-12-31 10:30:14 +05:30
);
}
2025-04-18 14:40:56 -04:00
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 (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 <ExitNodeTag key={tag} isEnabled={tag === 'exit-approved'} />;
case 'subnet-approved':
case 'subnet-waiting':
return <SubnetTag key={tag} isEnabled={tag === 'subnet-approved'} />;
case 'expired':
case 'no-expiry':
return (
<ExpiryTag
key={tag}
variant={tag}
expiry={node.expiry ?? undefined}
/>
);
case 'headplane-agent':
return <HeadplaneAgentTag />;
default:
return;
}
});
}