feat: use host info on the machines page
This commit is contained in:
parent
8263506713
commit
825fa6d854
@ -1,23 +1,37 @@
|
|||||||
import { Check, Copy } from 'lucide-react';
|
import { Check, Copy, Info } from 'lucide-react';
|
||||||
import cn from '~/utils/cn';
|
import cn from '~/utils/cn';
|
||||||
import toast from '~/utils/toast';
|
import toast from '~/utils/toast';
|
||||||
|
import Tooltip from './Tooltip';
|
||||||
|
|
||||||
export interface AttributeProps {
|
export interface AttributeProps {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
tooltip?: string;
|
||||||
isCopyable?: boolean;
|
isCopyable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Attribute({ name, value, isCopyable }: AttributeProps) {
|
export default function Attribute({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
tooltip,
|
||||||
|
isCopyable,
|
||||||
|
}: AttributeProps) {
|
||||||
return (
|
return (
|
||||||
<dl className="flex gap-1 items-center text-sm">
|
<dl className="flex gap-1 items-center text-sm">
|
||||||
<dt
|
<dt
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-1/3 sm:w-1/4 lg:w-1/3 shrink-0 min-w-0 truncate',
|
'w-1/3 sm:w-1/4 lg:w-1/3 shrink-0 min-w-0',
|
||||||
'text-headplane-500 dark:text-headplane-400',
|
'text-headplane-500 dark:text-headplane-400',
|
||||||
|
tooltip ? 'flex items-center gap-1' : undefined,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
|
{tooltip ? (
|
||||||
|
<Tooltip>
|
||||||
|
<Info className="size-4" />
|
||||||
|
<Tooltip.Body>{tooltip}</Tooltip.Body>
|
||||||
|
</Tooltip>
|
||||||
|
) : undefined}
|
||||||
</dt>
|
</dt>
|
||||||
<dd
|
<dd
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import Tooltip from '~/components/Tooltip';
|
|||||||
import type { LoadContext } from '~/server';
|
import type { LoadContext } from '~/server';
|
||||||
import type { Machine, Route, User } from '~/types';
|
import type { Machine, Route, User } from '~/types';
|
||||||
import cn from '~/utils/cn';
|
import cn from '~/utils/cn';
|
||||||
|
import { getOSInfo, getTSVersion } from '~/utils/host-info';
|
||||||
import { mapNodes } from '~/utils/node-info';
|
import { mapNodes } from '~/utils/node-info';
|
||||||
import { mapTagsToComponents, uiTagsForNode } from './components/machine-row';
|
import { mapTagsToComponents, uiTagsForNode } from './components/machine-row';
|
||||||
import MenuOptions from './components/menu';
|
import MenuOptions from './components/menu';
|
||||||
@ -49,12 +50,14 @@ export async function loader({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const [node] = mapNodes([machine.node], routes);
|
const [node] = mapNodes([machine.node], routes);
|
||||||
|
const lookup = await context.agents?.lookup([node.nodeKey]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
users,
|
users,
|
||||||
magic,
|
magic,
|
||||||
agent: context.agents?.agentID(),
|
agent: context.agents?.agentID(),
|
||||||
|
stats: lookup?.[node.nodeKey],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +66,7 @@ export async function action(request: ActionFunctionArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const { node, magic, users, agent } = useLoaderData<typeof loader>();
|
const { node, magic, users, agent, stats } = useLoaderData<typeof loader>();
|
||||||
const [showRouting, setShowRouting] = useState(false);
|
const [showRouting, setShowRouting] = useState(false);
|
||||||
|
|
||||||
const uiTags = useMemo(() => {
|
const uiTags = useMemo(() => {
|
||||||
@ -120,8 +123,8 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-xl font-medium mb-4 mt-8">Subnets & Routing</h2>
|
|
||||||
<Routes node={node} isOpen={showRouting} setIsOpen={setShowRouting} />
|
<Routes node={node} isOpen={showRouting} setIsOpen={setShowRouting} />
|
||||||
|
<h2 className="text-xl font-medium mt-8">Subnets & Routing</h2>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<p>
|
<p>
|
||||||
Subnets let you expose physical network routes onto Tailscale.{' '}
|
Subnets let you expose physical network routes onto Tailscale.{' '}
|
||||||
@ -240,40 +243,156 @@ export default function Page() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<h2 className="text-xl font-medium mb-4">Machine Details</h2>
|
<h2 className="text-xl font-medium">Machine Details</h2>
|
||||||
<Card variant="flat" className="w-full max-w-full">
|
<p className="mb-4">
|
||||||
<Attribute name="Creator" value={node.user.name} />
|
Information about this machine’s network. Used to debug connection
|
||||||
<Attribute name="Node ID" value={node.id} />
|
issues.
|
||||||
<Attribute name="Node Name" value={node.givenName} />
|
</p>
|
||||||
<Attribute name="Hostname" value={node.name} />
|
<Card
|
||||||
<Attribute isCopyable name="Node Key" value={node.nodeKey} />
|
variant="flat"
|
||||||
<Attribute
|
className="w-full max-w-full grid grid-cols-1 lg:grid-cols-2 gap-y-2 sm:gap-x-12"
|
||||||
suppressHydrationWarning
|
>
|
||||||
name="Created"
|
<div className="flex flex-col gap-1">
|
||||||
value={new Date(node.createdAt).toLocaleString()}
|
<Attribute name="Creator" value={node.user.name} />
|
||||||
/>
|
<Attribute name="Machine name" value={node.givenName} />
|
||||||
<Attribute
|
<Attribute
|
||||||
suppressHydrationWarning
|
tooltip="OS hostname is published by the machine’s operating system and is used as the default name for the machine."
|
||||||
name="Last Seen"
|
name="OS hostname"
|
||||||
value={new Date(node.lastSeen).toLocaleString()}
|
value={node.name}
|
||||||
/>
|
/>
|
||||||
<Attribute
|
{stats ? (
|
||||||
suppressHydrationWarning
|
<>
|
||||||
name="Expiry"
|
<Attribute name="OS" value={getOSInfo(stats)} />
|
||||||
value={
|
<Attribute name="Tailscale version" value={getTSVersion(stats)} />
|
||||||
node.expiry !== null
|
</>
|
||||||
? new Date(node.expiry).toLocaleString()
|
) : undefined}
|
||||||
: 'Never'
|
<Attribute
|
||||||
}
|
tooltip="ID for this machine. Used in the Headscale API."
|
||||||
/>
|
name="ID"
|
||||||
{magic ? (
|
value={node.id}
|
||||||
|
/>
|
||||||
<Attribute
|
<Attribute
|
||||||
isCopyable
|
isCopyable
|
||||||
name="Domain"
|
tooltip="Public key which uniquely identifies this machine."
|
||||||
value={`${node.givenName}.${magic}`}
|
name="Node key"
|
||||||
|
value={node.nodeKey}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
<Attribute
|
||||||
|
name="Created"
|
||||||
|
value={new Date(node.createdAt).toLocaleString()}
|
||||||
|
/>
|
||||||
|
<Attribute
|
||||||
|
name="Last Seen"
|
||||||
|
value={
|
||||||
|
node.online
|
||||||
|
? 'Connected'
|
||||||
|
: new Date(node.lastSeen).toLocaleString()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Attribute
|
||||||
|
name="Key expiry"
|
||||||
|
value={
|
||||||
|
node.expiry !== null
|
||||||
|
? new Date(node.expiry).toLocaleString()
|
||||||
|
: 'Never'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{magic ? (
|
||||||
|
<Attribute
|
||||||
|
isCopyable
|
||||||
|
name="Domain"
|
||||||
|
value={`${node.givenName}.${magic}`}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p className="uppercase text-sm font-semibold opacity-75">
|
||||||
|
Addresses
|
||||||
|
</p>
|
||||||
|
<Attribute
|
||||||
|
isCopyable
|
||||||
|
tooltip="This machine’s IPv4 address within your tailnet (your private Tailscale network)."
|
||||||
|
name="Tailscale IPv4"
|
||||||
|
value={getIpv4Address(node.ipAddresses)}
|
||||||
|
/>
|
||||||
|
<Attribute
|
||||||
|
isCopyable
|
||||||
|
tooltip="This machine’s IPv6 address within your tailnet (your private Tailscale network). Connections within your tailnet support IPv6 even if your ISP does not."
|
||||||
|
name="Tailscale IPv6"
|
||||||
|
value={getIpv6Address(node.ipAddresses)}
|
||||||
|
/>
|
||||||
|
<Attribute
|
||||||
|
isCopyable
|
||||||
|
tooltip="Users of your tailnet can use this DNS short name to access this machine."
|
||||||
|
name="Short domain"
|
||||||
|
value={node.givenName}
|
||||||
|
/>
|
||||||
|
{magic ? (
|
||||||
|
<Attribute
|
||||||
|
isCopyable
|
||||||
|
tooltip="Users of your tailnet can use this DNS name to access this machine."
|
||||||
|
name="Full domain"
|
||||||
|
value={`${node.givenName}.${magic}`}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
{stats ? (
|
||||||
|
<>
|
||||||
|
<p className="uppercase text-sm font-semibold opacity-75 mt-4">
|
||||||
|
Client Connectivity
|
||||||
|
</p>
|
||||||
|
<Attribute
|
||||||
|
tooltip="Whether the machine is behind a difficult NAT that varies the machine’s IP address depending on the destination."
|
||||||
|
name="Varies"
|
||||||
|
value={stats.NetInfo?.MappingVariesByDestIP ? 'Yes' : 'No'}
|
||||||
|
/>
|
||||||
|
<Attribute
|
||||||
|
tooltip="Whether the machine needs to traverse NATs with hairpinning."
|
||||||
|
name="Hairpinning"
|
||||||
|
value={stats.NetInfo?.HairPinning ? 'Yes' : 'No'}
|
||||||
|
/>
|
||||||
|
<Attribute
|
||||||
|
name="IPv6"
|
||||||
|
value={stats.NetInfo?.WorkingIPv6 ? 'Yes' : 'No'}
|
||||||
|
/>
|
||||||
|
<Attribute
|
||||||
|
name="UDP"
|
||||||
|
value={stats.NetInfo?.WorkingUDP ? 'Yes' : 'No'}
|
||||||
|
/>
|
||||||
|
<Attribute
|
||||||
|
name="UPnP"
|
||||||
|
value={stats.NetInfo?.UPnP ? 'Yes' : 'No'}
|
||||||
|
/>
|
||||||
|
<Attribute name="PCP" value={stats.NetInfo?.PCP ? 'Yes' : 'No'} />
|
||||||
|
<Attribute
|
||||||
|
name="NAT-PMP"
|
||||||
|
value={stats.NetInfo?.PMP ? 'Yes' : 'No'}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 '—';
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user