fix: switch to new aria components

This commit is contained in:
Aarnav Tale 2025-01-28 16:02:47 -05:00
parent 741f9aa6b5
commit 28e40eecbf
No known key found for this signature in database
12 changed files with 181 additions and 227 deletions

View File

@ -1,19 +1,17 @@
import { useMemo } from 'react';
import { import {
type ActionFunctionArgs, type ActionFunctionArgs,
type LoaderFunctionArgs, type LoaderFunctionArgs,
redirect, redirect,
} from 'react-router'; } from 'react-router';
import { Form, useActionData, useLoaderData } from 'react-router'; import { Form, useActionData, useLoaderData } from 'react-router';
import { useMemo } from 'react';
import Button from '~/components/Button'; import Button from '~/components/Button';
import Card from '~/components/Card'; import Card from '~/components/Card';
import Code from '~/components/Code'; import Code from '~/components/Code';
import TextField from '~/components/TextField'; import Input from '~/components/Input';
import type { Key } from '~/types'; import type { Key } from '~/types';
import { loadContext } from '~/utils/config/headplane'; import { loadContext } from '~/utils/config/headplane';
import { pull } from '~/utils/headscale'; import { pull } from '~/utils/headscale';
import { beginAuthFlow, getRedirectUri } from '~/utils/oidc';
import { commitSession, getSession } from '~/utils/sessions.server'; import { commitSession, getSession } from '~/utils/sessions.server';
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
@ -109,7 +107,7 @@ export default function Page() {
{actionData?.error ? ( {actionData?.error ? (
<p className="text-red-500 text-sm mb-2">{actionData.error}</p> <p className="text-red-500 text-sm mb-2">{actionData.error}</p>
) : undefined} ) : undefined}
<TextField <Input
isRequired isRequired
label="API Key" label="API Key"
name="api-key" name="api-key"

View File

@ -1,11 +1,10 @@
import { useState } from 'react'; import { useState } from 'react';
import { Input } from 'react-aria-components';
import { useFetcher } from 'react-router'; import { useFetcher } from 'react-router';
import Code from '~/components/Code'; import Code from '~/components/Code';
import Dialog from '~/components/Dialog'; import Dialog from '~/components/Dialog';
import Input from '~/components/Input';
import Spinner from '~/components/Spinner'; import Spinner from '~/components/Spinner';
import TextField from '~/components/TextField';
import { cn } from '~/utils/cn'; import { cn } from '~/utils/cn';
type Properties = { type Properties = {
@ -19,21 +18,15 @@ export default function Modal({ name, disabled }: Properties) {
const fetcher = useFetcher(); const fetcher = useFetcher();
return ( return (
<div className="flex flex-col w-2/3"> <div className="flex flex-col w-2/3 gap-y-4">
<h1 className="text-2xl font-medium mb-4">Tailnet Name</h1> <h1 className="text-2xl font-medium mb-2">Tailnet Name</h1>
<p className="text-gray-700 dark:text-gray-300"> <p>
This is the base domain name of your Tailnet. Devices are accessible at{' '} This is the base domain name of your Tailnet. Devices are accessible at{' '}
<Code>[device].{name}</Code> when Magic DNS is enabled. <Code>[device].{name}</Code> when Magic DNS is enabled.
</p> </p>
<Input <Input
readOnly isReadOnly
className={cn( className="w-3/5 font-medium text-sm"
'block px-2.5 py-1.5 w-1/2 rounded-lg my-4',
'border border-ui-200 dark:border-ui-600',
'dark:bg-ui-800 dark:text-ui-300 text-sm',
'outline-none',
)}
type="text"
value={name} value={name}
onFocus={(event) => { onFocus={(event) => {
event.target.select(); event.target.select();
@ -64,11 +57,10 @@ export default function Modal({ name, disabled }: Properties) {
Keep in mind that changing this can lead to all sorts of unexpected Keep in mind that changing this can lead to all sorts of unexpected
behavior and may break existing devices in your tailnet. behavior and may break existing devices in your tailnet.
</Dialog.Text> </Dialog.Text>
<TextField <Input
label="Tailnet name" label="Tailnet name"
placeholder="ts.net" placeholder="ts.net"
state={[newName, setNewName]} onChange={setNewName}
className="my-2"
/> />
</Dialog.Panel> </Dialog.Panel>
</Dialog> </Dialog>

View File

@ -1,10 +1,8 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useSubmit } from 'react-router'; import { useSubmit } from 'react-router';
import Code from '~/components/Code'; import Code from '~/components/Code';
import Dialog from '~/components/Dialog'; import Dialog from '~/components/Dialog';
import TextField from '~/components/TextField'; import Input from '~/components/Input';
import { cn } from '~/utils/cn';
interface Props { interface Props {
records: { name: string; type: 'A'; value: string }[]; records: { name: string; type: 'A'; value: string }[];
@ -56,21 +54,21 @@ export default function AddDNS({ records }: Props) {
<Dialog.Text> <Dialog.Text>
Enter the domain and IP address for the new DNS record. Enter the domain and IP address for the new DNS record.
</Dialog.Text> </Dialog.Text>
<TextField <div className="flex flex-col gap-2 mt-4">
<Input
isRequired isRequired
label="Domain" label="Domain"
placeholder="test.example.com" placeholder="test.example.com"
name="domain" onChange={setName}
state={[name, setName]} isInvalid={isDuplicate}
className={cn('mt-2', isDuplicate && 'outline outline-red-500')}
/> />
<TextField <Input
isRequired isRequired
label="IP Address" label="IP Address"
placeholder="101.101.101.101" placeholder="101.101.101.101"
name="ip" name="ip"
state={[ip, setIp]} onChange={setIp}
className={cn(isDuplicate && 'outline outline-red-500')} isInvalid={isDuplicate}
/> />
{isDuplicate ? ( {isDuplicate ? (
<p className="text-sm opacity-50"> <p className="text-sm opacity-50">
@ -78,6 +76,7 @@ export default function AddDNS({ records }: Props) {
<Code>{ip}</Code> already exists. <Code>{ip}</Code> already exists.
</p> </p>
) : undefined} ) : undefined}
</div>
</Dialog.Panel> </Dialog.Panel>
</Dialog> </Dialog>
); );

View File

@ -3,8 +3,8 @@ import { useState } from 'react';
import { useSubmit } from 'react-router'; import { useSubmit } from 'react-router';
import Dialog from '~/components/Dialog'; import Dialog from '~/components/Dialog';
import Input from '~/components/Input';
import Switch from '~/components/Switch'; import Switch from '~/components/Switch';
import TextField from '~/components/TextField';
import Tooltip from '~/components/Tooltip'; import Tooltip from '~/components/Tooltip';
import { cn } from '~/utils/cn'; import { cn } from '~/utils/cn';
@ -68,16 +68,12 @@ export default function AddNameserver({ nameservers }: Props) {
}} }}
> >
<Dialog.Title>Add nameserver</Dialog.Title> <Dialog.Title>Add nameserver</Dialog.Title>
<Dialog.Text className="font-semibold">Nameserver</Dialog.Text> <Input
<Dialog.Text className="text-sm"> label="Nameserver"
Use this IPv4 or IPv6 address to resolve names. description="Use this IPv4 or IPv6 address to resolve names."
</Dialog.Text>
<TextField
label="DNS Server"
placeholder="1.2.3.4" placeholder="1.2.3.4"
name="ns" name="ns"
state={[ns, setNs]} onChange={setNs}
className="mt-2 mb-8"
/> />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="block"> <div className="block">
@ -118,12 +114,11 @@ export default function AddNameserver({ nameservers }: Props) {
{split ? ( {split ? (
<> <>
<Dialog.Text className="font-semibold mt-8">Domain</Dialog.Text> <Dialog.Text className="font-semibold mt-8">Domain</Dialog.Text>
<TextField <Input
label="Domain" label="Domain"
placeholder="example.com" placeholder="example.com"
name="domain" name="domain"
state={[domain, setDomain]} onChange={setDomain}
className="my-2"
/> />
<Dialog.Text className="text-sm"> <Dialog.Text className="text-sm">
Only single-label or fully-qualified queries matching this suffix Only single-label or fully-qualified queries matching this suffix

View File

@ -79,6 +79,14 @@ export default function MachineRow({
tags.unshift('Subnets'); tags.unshift('Subnets');
} }
const ipOptions = useMemo(() => {
if (magic) {
return [...machine.ipAddresses, `${machine.givenName}.${prefix}`];
}
return machine.ipAddresses;
}, [magic, machine.ipAddresses]);
return ( return (
<tr <tr
key={machine.id} key={machine.id}
@ -108,46 +116,32 @@ export default function MachineRow({
<td className="py-2"> <td className="py-2">
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
{machine.ipAddresses[0]} {machine.ipAddresses[0]}
<Menu> <Menu placement="bottom end">
<Menu.IconButton className="bg-transparent" label="IP Addresses"> <Menu.IconButton className="bg-transparent" label="IP Addresses">
<ChevronDownIcon className="w-4 h-4" /> <ChevronDownIcon className="w-4 h-4" />
</Menu.IconButton> </Menu.IconButton>
<Menu.Items> <Menu.Panel
{machine.ipAddresses.map((ip) => ( onAction={async (key) => {
<Menu.ItemButton await navigator.clipboard.writeText(key.toString());
key={ip}
type="button"
className={cn(
'flex items-center gap-x-1.5 text-sm',
'justify-between w-full',
)}
onPress={async () => {
await navigator.clipboard.writeText(ip);
toast('Copied IP address to clipboard'); 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} {ip}
<CopyIcon className="w-3 h-3" /> <CopyIcon className="w-3 h-3" />
</Menu.ItemButton> </div>
</Menu.Item>
))} ))}
{magic ? ( </Menu.Section>
<Menu.ItemButton </Menu.Panel>
type="button"
className={cn(
'flex items-center gap-x-1.5 text-sm',
'justify-between w-full break-keep',
)}
onPress={async () => {
const ip = `${machine.givenName}.${prefix}`;
await navigator.clipboard.writeText(ip);
toast('Copied hostname to clipboard');
}}
>
{machine.givenName}.{prefix}
<CopyIcon className="w-3 h-3" />
</Menu.ItemButton>
) : undefined}
</Menu.Items>
</Menu> </Menu>
</div> </div>
</td> </td>

View File

@ -1,9 +1,8 @@
import { KebabHorizontalIcon } from '@primer/octicons-react'; import { Cog, Ellipsis } from 'lucide-react';
import React, { useState } from 'react'; import { useState } from 'react';
import MenuComponent from '~/components/Menu'; import Menu from '~/components/Menu';
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 Delete from '../dialogs/delete'; import Delete from '../dialogs/delete';
import Expire from '../dialogs/expire'; import Expire from '../dialogs/expire';
import Move from '../dialogs/move'; import Move from '../dialogs/move';
@ -16,17 +15,17 @@ interface MenuProps {
routes: Route[]; routes: Route[];
users: User[]; users: User[];
magic?: string; magic?: string;
buttonChild?: React.ReactNode; isFullButton?: boolean;
} }
type Modal = 'rename' | 'expire' | 'remove' | 'routes' | 'move' | 'tags' | null; type Modal = 'rename' | 'expire' | 'remove' | 'routes' | 'move' | 'tags' | null;
export default function Menu({ export default function MachineMenu({
machine, machine,
routes, routes,
magic, magic,
users, users,
buttonChild, isFullButton,
}: MenuProps) { }: MenuProps) {
const [modal, setModal] = useState<Modal>(null); const [modal, setModal] = useState<Modal>(null);
@ -97,44 +96,45 @@ export default function Menu({
/> />
)} )}
<MenuComponent> <Menu>
{buttonChild ?? ( {isFullButton ? (
<MenuComponent.Button <Menu.Button className="flex items-center gap-x-2">
<Cog className="h-5" />
<p>Machine Settings</p>
</Menu.Button>
) : (
<Menu.IconButton
label="Machine Options"
className={cn( className={cn(
'flex items-center justify-center', 'py-0.5 w-10 bg-transparent border-transparent',
'border border-transparent rounded-lg py-0.5 w-10', 'border group-hover:border-headplane-200',
'group-hover:border-gray-200 dark:group-hover:border-zinc-700', 'dark:group-hover:border-headplane-700',
)} )}
> >
<KebabHorizontalIcon className="w-5" /> <Ellipsis className="h-5" />
</MenuComponent.Button> </Menu.IconButton>
)} )}
<MenuComponent.Items> <Menu.Panel onAction={(key) => setModal(key as Modal)}>
<MenuComponent.ItemButton onPress={() => setModal('rename')}> <Menu.Section>
Edit machine name <Menu.Item key="rename">Edit machine name</Menu.Item>
</MenuComponent.ItemButton> <Menu.Item key="routes">Edit route settings</Menu.Item>
<MenuComponent.ItemButton onPress={() => setModal('routes')}> <Menu.Item key="tags">Edit ACL tags</Menu.Item>
Edit route settings <Menu.Item key="move">Change owner</Menu.Item>
</MenuComponent.ItemButton> </Menu.Section>
<MenuComponent.ItemButton onPress={() => setModal('tags')}> <Menu.Section>
Edit ACL tags {expired ? (
</MenuComponent.ItemButton> <></>
<MenuComponent.ItemButton onPress={() => setModal('move')}> ) : (
Change owner <Menu.Item key="expire" textValue="Expire">
</MenuComponent.ItemButton> <p className="text-red-500 dark:text-red-400">Expire</p>
{expired ? undefined : ( </Menu.Item>
<MenuComponent.ItemButton onPress={() => setModal('expire')}>
Expire
</MenuComponent.ItemButton>
)} )}
<MenuComponent.ItemButton <Menu.Item key="remove" textValue="Remove">
className="text-red-500 dark:text-red-400" <p className="text-red-500 dark:text-red-400">Remove</p>
onPress={() => setModal('remove')} </Menu.Item>
> </Menu.Section>
Remove </Menu.Panel>
</MenuComponent.ItemButton> </Menu>
</MenuComponent.Items>
</MenuComponent>
</> </>
); );
} }

View File

@ -26,9 +26,7 @@ export default function Move({ machine, users, isOpen, setIsOpen }: MoveProps) {
defaultSelectedKey={machine.user.id} defaultSelectedKey={machine.user.id}
> >
{users.map((user) => ( {users.map((user) => (
<Select.Item key={user.id} id={user.name}> <Select.Item key={user.id}>{user.name}</Select.Item>
{user.name}
</Select.Item>
))} ))}
</Select> </Select>
</Dialog.Panel> </Dialog.Panel>

View File

@ -1,14 +1,11 @@
import { KeyIcon, ServerIcon } from '@primer/octicons-react'; import { Computer, KeySquare } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { Link, useFetcher } from 'react-router'; import { useNavigate } from 'react-router';
import Code from '~/components/Code'; import Code from '~/components/Code';
import Dialog from '~/components/Dialog'; import Dialog from '~/components/Dialog';
import Input from '~/components/Input';
import Menu from '~/components/Menu'; import Menu from '~/components/Menu';
import Select from '~/components/Select'; import Select from '~/components/Select';
import Spinner from '~/components/Spinner';
import TextField from '~/components/TextField';
import { toast } from '~/components/Toaster';
import type { User } from '~/types'; import type { User } from '~/types';
export interface NewProps { export interface NewProps {
@ -17,32 +14,14 @@ export interface NewProps {
} }
export default function New(data: NewProps) { export default function New(data: NewProps) {
const fetcher = useFetcher<{ success?: boolean }>();
const [pushDialog, setPushDialog] = useState(false); const [pushDialog, setPushDialog] = useState(false);
const [mkey, setMkey] = useState(''); const [mkey, setMkey] = useState('');
const [user, setUser] = useState(''); const navigate = useNavigate();
const [toasted, setToasted] = useState(false);
useEffect(() => {
if (!fetcher.data || toasted) {
return;
}
if (fetcher.data.success) {
toast('Registered new machine');
} else {
toast('Failed to register machine due to an invalid key');
}
setToasted(true);
}, [fetcher.data, toasted]);
return ( return (
<> <>
<Dialog isOpen={pushDialog} onOpenChange={setPushDialog}> <Dialog isOpen={pushDialog} onOpenChange={setPushDialog}>
<Dialog.Panel <Dialog.Panel isDisabled={!mkey.trim().startsWith('mkey:')}>
isDisabled={!mkey || !mkey.trim().startsWith('mkey:') || !user}
>
<Dialog.Title>Register Machine Key</Dialog.Title> <Dialog.Title>Register Machine Key</Dialog.Title>
<Dialog.Text className="mb-4"> <Dialog.Text className="mb-4">
The machine key is given when you run{' '} The machine key is given when you run{' '}
@ -54,41 +33,55 @@ export default function New(data: NewProps) {
</Dialog.Text> </Dialog.Text>
<input type="hidden" name="_method" value="register" /> <input type="hidden" name="_method" value="register" />
<input type="hidden" name="id" value="_" /> <input type="hidden" name="id" value="_" />
<TextField <Input
isRequired
label="Machine Key" label="Machine Key"
placeholder="mkey:ff....." placeholder="mkey:ff....."
validationBehavior="native"
name="mkey" name="mkey"
state={[mkey, setMkey]} onChange={setMkey}
className="my-2 font-mono"
/> />
<Select <Select
isRequired
label="Owner" label="Owner"
name="user" name="user"
placeholder="Select a user" placeholder="Select a user"
state={[user, setUser]}
> >
{data.users.map((user) => ( {data.users.map((user) => (
<Select.Item key={user.id} id={user.name}> <Select.Item key={user.id}>{user.name}</Select.Item>
{user.name}
</Select.Item>
))} ))}
</Select> </Select>
</Dialog.Panel> </Dialog.Panel>
</Dialog> </Dialog>
<Menu> <Menu>
<Menu.Button variant="heavy">Add Device</Menu.Button> <Menu.Button variant="heavy">Add Device</Menu.Button>
<Menu.Items> <Menu.Panel
<Menu.ItemButton onPress={() => setPushDialog(true)}> onAction={(key) => {
<ServerIcon className="w-4 h-4 mr-2" /> if (key === 'register') {
setPushDialog(true);
return;
}
if (key === 'pre-auth') {
navigate('/settings/auth-keys');
}
}}
>
<Menu.Section>
<Menu.Item key="register" textValue="Register Machine Key">
<div className="flex items-center gap-x-3">
<Computer className="w-4" />
Register Machine Key Register Machine Key
</Menu.ItemButton> </div>
<Menu.ItemButton> </Menu.Item>
<Link to="/settings/auth-keys"> <Menu.Item key="pre-auth" textValue="Generate Pre-auth Key">
<KeyIcon className="w-4 h-4 mr-2" /> <div className="flex items-center gap-x-3">
<KeySquare className="w-4" />
Generate Pre-auth Key Generate Pre-auth Key
</Link> </div>
</Menu.ItemButton> </Menu.Item>
</Menu.Items> </Menu.Section>
</Menu.Panel>
</Menu> </Menu>
</> </>
); );

View File

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import Code from '~/components/Code'; import Code from '~/components/Code';
import Dialog from '~/components/Dialog'; import Dialog from '~/components/Dialog';
import TextField from '~/components/TextField'; import Input from '~/components/Input';
import type { Machine } from '~/types'; import type { Machine } from '~/types';
interface RenameProps { interface RenameProps {
@ -29,11 +29,10 @@ export default function Rename({
</Dialog.Text> </Dialog.Text>
<input type="hidden" name="_method" value="rename" /> <input type="hidden" name="_method" value="rename" />
<input type="hidden" name="id" value={machine.id} /> <input type="hidden" name="id" value={machine.id} />
<TextField <Input
label="Machine name" label="Machine name"
placeholder="Machine name" placeholder="Machine name"
name="name" name="name"
className="my-2"
defaultValue={machine.givenName} defaultValue={machine.givenName}
onChange={setName} onChange={setName}
/> />

View File

@ -138,24 +138,11 @@ export default function Page() {
</span> </span>
<MenuOptions <MenuOptions
isFullButton
machine={machine} machine={machine}
routes={routes} routes={routes}
users={users} users={users}
magic={magic} magic={magic}
buttonChild={
<Menu.Button
className={cn(
'flex items-center justify-center gap-x-2',
'bg-main-200 dark:bg-main-700/30',
'hover:bg-main-300 dark:hover:bg-main-600/30',
'text-ui-700 dark:text-ui-300 mb-2',
'w-fit text-sm rounded-lg px-3 py-2',
)}
>
<GearIcon className="w-5" />
Machine Settings
</Menu.Button>
}
/> />
</div> </div>
<div className="flex gap-1 mb-4"> <div className="flex gap-1 mb-4">
@ -196,7 +183,8 @@ export default function Page() {
<Routes <Routes
machine={machine} machine={machine}
routes={routes} routes={routes}
state={[showRouting, setShowRouting]} isOpen={showRouting}
setIsOpen={setShowRouting}
/> />
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<p> <p>

View File

@ -1,21 +1,18 @@
import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router';
import { useLoaderData } from 'react-router';
import { useLiveData } from '~/utils/useLiveData';
import { getSession } from '~/utils/sessions.server';
import { Link as RemixLink } from 'react-router';
import type { PreAuthKey, User } from '~/types';
import { pull, post } from '~/utils/headscale';
import { loadContext } from '~/utils/config/headplane';
import { useState } from 'react'; import { useState } from 'react';
import { send } from '~/utils/res'; import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
import { useLoaderData } from 'react-router';
import { Link as RemixLink } from 'react-router';
import Link from '~/components/Link'; import Link from '~/components/Link';
import TableList from '~/components/TableList';
import Select from '~/components/Select'; import Select from '~/components/Select';
import Switch from '~/components/Switch'; import TableList from '~/components/TableList';
import type { PreAuthKey, User } from '~/types';
import AddPreAuthKey from './dialogs/new'; import { loadContext } from '~/utils/config/headplane';
import { post, pull } from '~/utils/headscale';
import { send } from '~/utils/res';
import { getSession } from '~/utils/sessions.server';
import { useLiveData } from '~/utils/useLiveData';
import AuthKeyRow from './components/key'; import AuthKeyRow from './components/key';
import AddPreAuthKey from './dialogs/new';
export async function action({ request }: ActionFunctionArgs) { export async function action({ request }: ActionFunctionArgs) {
const session = await getSession(request.headers.get('Cookie')); const session = await getSession(request.headers.get('Cookie'));
@ -156,6 +153,7 @@ export default function Page() {
return true; return true;
}); });
// TODO: Fix the selects
return ( return (
<div className="flex flex-col w-2/3"> <div className="flex flex-col w-2/3">
<p className="mb-8 text-md"> <p className="mb-8 text-md">
@ -202,13 +200,13 @@ export default function Page() {
<Select <Select
label="Filter by status" label="Filter by status"
placeholder="Select a status" placeholder="Select a status"
state={[status, setStatus]} defaultSelectedKey="Active"
> >
<Select.Item id="All">All</Select.Item> <Select.Item key="All">All</Select.Item>
<Select.Item id="Active">Active</Select.Item> <Select.Item>Active</Select.Item>
<Select.Item id="Used/Expired">Used/Expired</Select.Item> <Select.Item>Used/Expired</Select.Item>
<Select.Item id="Reusable">Reusable</Select.Item> <Select.Item>Reusable</Select.Item>
<Select.Item id="Ephemeral">Ephemeral</Select.Item> <Select.Item>Ephemeral</Select.Item>
</Select> </Select>
</div> </div>
</div> </div>

View File

@ -1,9 +1,9 @@
import type { PreAuthKey } from '~/types';
import { toast } from '~/components/Toaster'; import { toast } from '~/components/Toaster';
import type { PreAuthKey } from '~/types';
import Code from '~/components/Code';
import Button from '~/components/Button';
import Attribute from '~/components/Attribute'; import Attribute from '~/components/Attribute';
import Button from '~/components/Button';
import Code from '~/components/Code';
import ExpireKey from '../dialogs/expire'; import ExpireKey from '../dialogs/expire';
interface Props { interface Props {
@ -31,7 +31,7 @@ export default function AuthKeyRow({ authKey, server }: Props) {
tailscale up --login-server {server} --authkey {authKey.key} tailscale up --login-server {server} --authkey {authKey.key}
</Code> </Code>
<div className="flex gap-4 items-center"> <div className="flex gap-4 items-center">
{authKey.used && !authKey.reusable || {(authKey.used && !authKey.reusable) ||
new Date(authKey.expiration) < new Date() ? undefined : ( new Date(authKey.expiration) < new Date() ? undefined : (
<ExpireKey authKey={authKey} /> <ExpireKey authKey={authKey} />
)} )}