2025-02-04 17:21:03 -05:00
|
|
|
|
import { CheckCircle, CircleSlash, Info, UserCircle } from 'lucide-react';
|
2024-12-31 10:30:14 +05:30
|
|
|
|
import { useMemo, useState } from 'react';
|
2025-01-23 11:16:56 -05:00
|
|
|
|
import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
|
|
|
|
|
|
import { Link as RemixLink, useLoaderData } from 'react-router';
|
2025-04-18 14:40:56 -04:00
|
|
|
|
import { mapTag } from 'yaml/util';
|
2024-12-31 10:30:14 +05:30
|
|
|
|
import Attribute from '~/components/Attribute';
|
|
|
|
|
|
import Button from '~/components/Button';
|
|
|
|
|
|
import Card from '~/components/Card';
|
2025-01-23 11:28:42 -05:00
|
|
|
|
import Chip from '~/components/Chip';
|
2025-01-23 11:16:56 -05:00
|
|
|
|
import Link from '~/components/Link';
|
2024-12-31 10:30:14 +05:30
|
|
|
|
import StatusCircle from '~/components/StatusCircle';
|
2025-01-23 11:16:56 -05:00
|
|
|
|
import Tooltip from '~/components/Tooltip';
|
2025-03-22 01:36:27 -04:00
|
|
|
|
import type { LoadContext } from '~/server';
|
2024-12-31 10:31:50 +05:30
|
|
|
|
import type { Machine, Route, User } from '~/types';
|
2025-01-28 17:46:16 -05:00
|
|
|
|
import cn from '~/utils/cn';
|
2025-04-18 14:40:56 -04:00
|
|
|
|
import { mapNodes } from '~/utils/node-info';
|
|
|
|
|
|
import { mapTagsToComponents, uiTagsForNode } from './components/machine-row';
|
2024-12-31 10:30:14 +05:30
|
|
|
|
import MenuOptions from './components/menu';
|
|
|
|
|
|
import Routes from './dialogs/routes';
|
2025-04-03 00:14:51 -04:00
|
|
|
|
import { machineAction } from './machine-actions';
|
2024-06-02 01:33:40 -04:00
|
|
|
|
|
2025-03-22 01:36:27 -04:00
|
|
|
|
export async function loader({
|
|
|
|
|
|
request,
|
|
|
|
|
|
params,
|
|
|
|
|
|
context,
|
|
|
|
|
|
}: LoaderFunctionArgs<LoadContext>) {
|
|
|
|
|
|
const session = await context.sessions.auth(request);
|
2024-03-26 09:28:52 -04:00
|
|
|
|
if (!params.id) {
|
2024-12-31 10:30:14 +05:30
|
|
|
|
throw new Error('No machine ID provided');
|
2024-03-26 09:28:52 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-31 10:30:14 +05:30
|
|
|
|
let magic: string | undefined;
|
2025-03-22 01:36:27 -04:00
|
|
|
|
if (context.hs.readable()) {
|
|
|
|
|
|
if (context.hs.c?.dns.magic_dns) {
|
|
|
|
|
|
magic = context.hs.c.dns.base_domain;
|
2024-06-02 01:33:40 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-18 14:40:56 -04:00
|
|
|
|
const [machine, { routes }, { users }] = await Promise.all([
|
2025-03-22 01:36:27 -04:00
|
|
|
|
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')!),
|
2024-12-31 10:30:14 +05:30
|
|
|
|
]);
|
2024-06-02 01:33:40 -04:00
|
|
|
|
|
2025-04-18 14:40:56 -04:00
|
|
|
|
const [node] = mapNodes([machine.node], routes);
|
|
|
|
|
|
|
2024-06-02 01:33:40 -04:00
|
|
|
|
return {
|
2025-04-18 14:40:56 -04:00
|
|
|
|
node,
|
|
|
|
|
|
users,
|
2024-06-02 01:33:40 -04:00
|
|
|
|
magic,
|
2025-04-18 14:40:56 -04:00
|
|
|
|
agent: context.agents?.agentID(),
|
2024-12-31 10:30:14 +05:30
|
|
|
|
};
|
2024-03-26 09:28:52 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-22 01:36:27 -04:00
|
|
|
|
export async function action(request: ActionFunctionArgs) {
|
2025-04-03 00:14:51 -04:00
|
|
|
|
return machineAction(request);
|
2024-06-23 01:05:03 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-26 09:28:52 -04:00
|
|
|
|
export default function Page() {
|
2025-04-18 14:40:56 -04:00
|
|
|
|
const { node, magic, users, agent } = useLoaderData<typeof loader>();
|
2025-01-19 17:31:46 +00:00
|
|
|
|
const [showRouting, setShowRouting] = useState(false);
|
2024-03-26 09:28:52 -04:00
|
|
|
|
|
2025-04-18 14:40:56 -04:00
|
|
|
|
const uiTags = useMemo(() => {
|
|
|
|
|
|
const tags = uiTagsForNode(node, agent === node.nodeKey);
|
|
|
|
|
|
return tags;
|
|
|
|
|
|
}, [node, agent]);
|
2024-11-07 13:04:53 -05:00
|
|
|
|
|
2024-03-26 09:28:52 -04:00
|
|
|
|
return (
|
|
|
|
|
|
<div>
|
2024-06-02 01:33:40 -04:00
|
|
|
|
<p className="mb-8 text-md">
|
2024-12-31 10:30:14 +05:30
|
|
|
|
<RemixLink to="/machines" className="font-medium">
|
2024-03-26 09:28:52 -04:00
|
|
|
|
All Machines
|
2024-11-07 13:04:53 -05:00
|
|
|
|
</RemixLink>
|
2024-12-31 10:30:14 +05:30
|
|
|
|
<span className="mx-2">/</span>
|
2025-04-18 14:40:56 -04:00
|
|
|
|
{node.givenName}
|
2024-03-26 09:28:52 -04:00
|
|
|
|
</p>
|
2024-12-31 10:30:14 +05:30
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
2025-02-04 17:21:03 -05:00
|
|
|
|
'flex justify-between items-center pb-2',
|
|
|
|
|
|
'border-b border-headplane-100 dark:border-headplane-800',
|
2024-12-31 10:30:14 +05:30
|
|
|
|
)}
|
|
|
|
|
|
>
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<span className="flex items-baseline gap-x-4 text-sm">
|
2025-04-18 14:40:56 -04:00
|
|
|
|
<h1 className="text-2xl font-medium">{node.givenName}</h1>
|
|
|
|
|
|
<StatusCircle isOnline={node.online} className="w-4 h-4" />
|
2024-06-02 01:33:40 -04:00
|
|
|
|
</span>
|
2025-04-18 14:40:56 -04:00
|
|
|
|
<MenuOptions isFullButton node={node} users={users} magic={magic} />
|
2024-06-02 01:33:40 -04:00
|
|
|
|
</div>
|
2024-11-07 13:04:53 -05:00
|
|
|
|
<div className="flex gap-1 mb-4">
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<div className="border-r border-headplane-100 dark:border-headplane-800 p-2 pr-4">
|
|
|
|
|
|
<span className="text-sm text-headplane-600 dark:text-headplane-300 flex items-center gap-x-1">
|
2024-11-07 13:04:53 -05:00
|
|
|
|
Managed by
|
|
|
|
|
|
<Tooltip>
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<Info className="p-1" />
|
2024-11-07 13:04:53 -05:00
|
|
|
|
<Tooltip.Body>
|
|
|
|
|
|
By default, a machine’s permissions match its creator’s.
|
|
|
|
|
|
</Tooltip.Body>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div className="flex items-center gap-x-2.5 mt-1">
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<UserCircle />
|
2025-04-18 14:40:56 -04:00
|
|
|
|
{node.user.name}
|
2024-11-07 13:04:53 -05:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-04-18 14:40:56 -04:00
|
|
|
|
<div className="p-2 pl-4">
|
|
|
|
|
|
<p className="text-sm text-headplane-600 dark:text-headplane-300">
|
|
|
|
|
|
Status
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="flex gap-1 mt-1 mb-8">
|
|
|
|
|
|
{mapTagsToComponents(node, uiTags)}
|
|
|
|
|
|
{node.validTags.map((tag) => (
|
|
|
|
|
|
<Chip key={tag} text={tag} />
|
|
|
|
|
|
))}
|
2024-11-07 13:04:53 -05:00
|
|
|
|
</div>
|
2025-04-18 14:40:56 -04:00
|
|
|
|
</div>
|
2024-11-07 13:04:53 -05:00
|
|
|
|
</div>
|
2024-12-31 10:30:14 +05:30
|
|
|
|
<h2 className="text-xl font-medium mb-4 mt-8">Subnets & Routing</h2>
|
2025-04-18 14:40:56 -04:00
|
|
|
|
<Routes node={node} isOpen={showRouting} setIsOpen={setShowRouting} />
|
2024-11-07 13:04:53 -05:00
|
|
|
|
<div className="flex items-center justify-between mb-4">
|
|
|
|
|
|
<p>
|
2024-12-31 10:30:14 +05:30
|
|
|
|
Subnets let you expose physical network routes onto Tailscale.{' '}
|
2024-11-07 13:04:53 -05:00
|
|
|
|
<Link
|
|
|
|
|
|
to="https://tailscale.com/kb/1019/subnets"
|
|
|
|
|
|
name="Tailscale Subnets Documentation"
|
|
|
|
|
|
>
|
|
|
|
|
|
Learn More
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</p>
|
2025-01-23 11:16:56 -05:00
|
|
|
|
<Button onPress={() => setShowRouting(true)}>Review</Button>
|
2024-11-07 13:04:53 -05:00
|
|
|
|
</div>
|
|
|
|
|
|
<Card
|
|
|
|
|
|
variant="flat"
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
'w-full max-w-full grid sm:grid-cols-2',
|
|
|
|
|
|
'md:grid-cols-4 gap-8 mr-2 text-sm mb-8',
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div>
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<span className="text-headplane-600 dark:text-headplane-300 flex items-center gap-x-1">
|
2024-11-07 13:04:53 -05:00
|
|
|
|
Approved
|
|
|
|
|
|
<Tooltip>
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<Info className="w-3.5 h-3.5" />
|
2024-11-07 13:04:53 -05:00
|
|
|
|
<Tooltip.Body>
|
2024-12-31 10:30:14 +05:30
|
|
|
|
Traffic to these routes are being routed through this machine.
|
2024-11-07 13:04:53 -05:00
|
|
|
|
</Tooltip.Body>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div className="mt-1">
|
2025-04-18 14:40:56 -04:00
|
|
|
|
{node.customRouting.subnetApprovedRoutes.length === 0 ? (
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<span className="opacity-50">—</span>
|
2024-11-07 13:04:53 -05:00
|
|
|
|
) : (
|
|
|
|
|
|
<ul className="leading-normal">
|
2025-04-18 14:40:56 -04:00
|
|
|
|
{node.customRouting.subnetApprovedRoutes.map((route) => (
|
2024-12-31 10:30:14 +05:30
|
|
|
|
<li key={route.id}>{route.prefix}</li>
|
2024-11-07 13:04:53 -05:00
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
2025-01-19 17:31:46 +00:00
|
|
|
|
onPress={() => setShowRouting(true)}
|
2024-06-02 01:33:40 -04:00
|
|
|
|
className={cn(
|
2025-01-19 17:31:46 +00:00
|
|
|
|
'px-1.5 py-0.5 rounded-md mt-1.5',
|
2024-11-07 13:04:53 -05:00
|
|
|
|
'text-blue-500 dark:text-blue-400',
|
2024-06-02 01:33:40 -04:00
|
|
|
|
)}
|
|
|
|
|
|
>
|
2024-11-07 13:04:53 -05:00
|
|
|
|
Edit
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<span className="text-headplane-600 dark:text-headplane-300 flex items-center gap-x-1">
|
2024-11-07 13:04:53 -05:00
|
|
|
|
Awaiting Approval
|
|
|
|
|
|
<Tooltip>
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<Info className="w-3.5 h-3.5" />
|
2024-11-07 13:04:53 -05:00
|
|
|
|
<Tooltip.Body>
|
2024-12-31 10:30:14 +05:30
|
|
|
|
This machine is advertising these routes, but they must be
|
|
|
|
|
|
approved before traffic will be routed to them.
|
2024-11-07 13:04:53 -05:00
|
|
|
|
</Tooltip.Body>
|
|
|
|
|
|
</Tooltip>
|
2024-06-02 01:33:40 -04:00
|
|
|
|
</span>
|
2024-11-07 13:04:53 -05:00
|
|
|
|
<div className="mt-1">
|
2025-04-18 14:40:56 -04:00
|
|
|
|
{node.customRouting.subnetWaitingRoutes.length === 0 ? (
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<span className="opacity-50">—</span>
|
2024-11-07 13:04:53 -05:00
|
|
|
|
) : (
|
|
|
|
|
|
<ul className="leading-normal">
|
2025-04-18 14:40:56 -04:00
|
|
|
|
{node.customRouting.subnetWaitingRoutes.map((route) => (
|
2024-12-31 10:30:14 +05:30
|
|
|
|
<li key={route.id}>{route.prefix}</li>
|
2024-11-07 13:04:53 -05:00
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
2025-01-19 17:31:46 +00:00
|
|
|
|
onPress={() => setShowRouting(true)}
|
2024-11-07 13:04:53 -05:00
|
|
|
|
className={cn(
|
2025-01-19 17:31:46 +00:00
|
|
|
|
'px-1.5 py-0.5 rounded-md mt-1.5',
|
2024-11-07 13:04:53 -05:00
|
|
|
|
'text-blue-500 dark:text-blue-400',
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
Edit
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<span className="text-headplane-600 dark:text-headplane-300 flex items-center gap-x-1">
|
2024-11-07 13:04:53 -05:00
|
|
|
|
Exit Node
|
|
|
|
|
|
<Tooltip>
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<Info className="w-3.5 h-3.5" />
|
2024-11-07 13:04:53 -05:00
|
|
|
|
<Tooltip.Body>
|
2024-12-31 10:30:14 +05:30
|
|
|
|
Whether this machine can act as an exit node for your tailnet.
|
2024-11-07 13:04:53 -05:00
|
|
|
|
</Tooltip.Body>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div className="mt-1">
|
2025-04-18 14:40:56 -04:00
|
|
|
|
{node.customRouting.exitRoutes.length === 0 ? (
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<span className="opacity-50">—</span>
|
2025-04-18 14:40:56 -04:00
|
|
|
|
) : node.customRouting.exitApproved ? (
|
2024-11-07 13:04:53 -05:00
|
|
|
|
<span className="flex items-center gap-x-1">
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<CheckCircle className="w-3.5 h-3.5 text-green-700" />
|
2024-11-07 13:04:53 -05:00
|
|
|
|
Allowed
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="flex items-center gap-x-1">
|
2025-02-04 17:21:03 -05:00
|
|
|
|
<CircleSlash className="w-3.5 h-3.5 text-red-700" />
|
2024-11-07 13:04:53 -05:00
|
|
|
|
Awaiting Approval
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
2025-01-19 17:31:46 +00:00
|
|
|
|
onPress={() => setShowRouting(true)}
|
2024-11-07 13:04:53 -05:00
|
|
|
|
className={cn(
|
2025-01-19 17:31:46 +00:00
|
|
|
|
'px-1.5 py-0.5 rounded-md mt-1.5',
|
2024-11-07 13:04:53 -05:00
|
|
|
|
'text-blue-500 dark:text-blue-400',
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
Edit
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
2024-12-31 10:30:14 +05:30
|
|
|
|
<h2 className="text-xl font-medium mb-4">Machine Details</h2>
|
2024-06-02 01:33:40 -04:00
|
|
|
|
<Card variant="flat" className="w-full max-w-full">
|
2025-04-18 14:40:56 -04:00
|
|
|
|
<Attribute name="Creator" value={node.user.name} />
|
|
|
|
|
|
<Attribute name="Node ID" value={node.id} />
|
|
|
|
|
|
<Attribute name="Node Name" value={node.givenName} />
|
|
|
|
|
|
<Attribute name="Hostname" value={node.name} />
|
|
|
|
|
|
<Attribute isCopyable name="Node Key" value={node.nodeKey} />
|
2024-03-26 09:28:52 -04:00
|
|
|
|
<Attribute
|
2025-04-03 16:54:12 -04:00
|
|
|
|
suppressHydrationWarning
|
2024-06-02 01:33:40 -04:00
|
|
|
|
name="Created"
|
2025-04-18 14:40:56 -04:00
|
|
|
|
value={new Date(node.createdAt).toLocaleString()}
|
2024-03-26 09:28:52 -04:00
|
|
|
|
/>
|
|
|
|
|
|
<Attribute
|
2025-04-03 16:54:12 -04:00
|
|
|
|
suppressHydrationWarning
|
2024-06-02 01:33:40 -04:00
|
|
|
|
name="Last Seen"
|
2025-04-18 14:40:56 -04:00
|
|
|
|
value={new Date(node.lastSeen).toLocaleString()}
|
2024-03-26 09:28:52 -04:00
|
|
|
|
/>
|
|
|
|
|
|
<Attribute
|
2025-04-03 16:54:12 -04:00
|
|
|
|
suppressHydrationWarning
|
2024-06-02 01:33:40 -04:00
|
|
|
|
name="Expiry"
|
2025-04-18 14:40:56 -04:00
|
|
|
|
value={
|
|
|
|
|
|
node.expiry !== null
|
|
|
|
|
|
? new Date(node.expiry).toLocaleString()
|
|
|
|
|
|
: 'Never'
|
|
|
|
|
|
}
|
2024-03-26 09:28:52 -04:00
|
|
|
|
/>
|
2024-12-31 10:30:14 +05:30
|
|
|
|
{magic ? (
|
|
|
|
|
|
<Attribute
|
|
|
|
|
|
isCopyable
|
|
|
|
|
|
name="Domain"
|
2025-04-18 14:40:56 -04:00
|
|
|
|
value={`${node.givenName}.${magic}`}
|
2024-12-31 10:30:14 +05:30
|
|
|
|
/>
|
|
|
|
|
|
) : undefined}
|
2024-03-29 15:41:10 -04:00
|
|
|
|
</Card>
|
2024-03-26 09:28:52 -04:00
|
|
|
|
</div>
|
2024-12-31 10:30:14 +05:30
|
|
|
|
);
|
2024-03-26 09:28:52 -04:00
|
|
|
|
}
|