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

View File

@ -1,11 +1,10 @@
import { useState } from 'react';
import { Input } from 'react-aria-components';
import { useFetcher } from 'react-router';
import Code from '~/components/Code';
import Dialog from '~/components/Dialog';
import Input from '~/components/Input';
import Spinner from '~/components/Spinner';
import TextField from '~/components/TextField';
import { cn } from '~/utils/cn';
type Properties = {
@ -19,21 +18,15 @@ export default function Modal({ name, disabled }: Properties) {
const fetcher = useFetcher();
return (
<div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Tailnet Name</h1>
<p className="text-gray-700 dark:text-gray-300">
<div className="flex flex-col w-2/3 gap-y-4">
<h1 className="text-2xl font-medium mb-2">Tailnet Name</h1>
<p>
This is the base domain name of your Tailnet. Devices are accessible at{' '}
<Code>[device].{name}</Code> when Magic DNS is enabled.
</p>
<Input
readOnly
className={cn(
'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"
isReadOnly
className="w-3/5 font-medium text-sm"
value={name}
onFocus={(event) => {
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
behavior and may break existing devices in your tailnet.
</Dialog.Text>
<TextField
<Input
label="Tailnet name"
placeholder="ts.net"
state={[newName, setNewName]}
className="my-2"
onChange={setNewName}
/>
</Dialog.Panel>
</Dialog>

View File

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

View File

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

View File

@ -79,6 +79,14 @@ export default function MachineRow({
tags.unshift('Subnets');
}
const ipOptions = useMemo(() => {
if (magic) {
return [...machine.ipAddresses, `${machine.givenName}.${prefix}`];
}
return machine.ipAddresses;
}, [magic, machine.ipAddresses]);
return (
<tr
key={machine.id}
@ -108,46 +116,32 @@ export default function MachineRow({
<td className="py-2">
<div className="flex items-center gap-x-1">
{machine.ipAddresses[0]}
<Menu>
<Menu placement="bottom end">
<Menu.IconButton className="bg-transparent" label="IP Addresses">
<ChevronDownIcon className="w-4 h-4" />
</Menu.IconButton>
<Menu.Items>
{machine.ipAddresses.map((ip) => (
<Menu.ItemButton
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');
}}
>
{ip}
<CopyIcon className="w-3 h-3" />
</Menu.ItemButton>
))}
{magic ? (
<Menu.ItemButton
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.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>

View File

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

View File

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

View File

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

View File

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

View File

@ -138,24 +138,11 @@ export default function Page() {
</span>
<MenuOptions
isFullButton
machine={machine}
routes={routes}
users={users}
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 className="flex gap-1 mb-4">
@ -196,7 +183,8 @@ export default function Page() {
<Routes
machine={machine}
routes={routes}
state={[showRouting, setShowRouting]}
isOpen={showRouting}
setIsOpen={setShowRouting}
/>
<div className="flex items-center justify-between mb-4">
<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 { 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 TableList from '~/components/TableList';
import Select from '~/components/Select';
import Switch from '~/components/Switch';
import AddPreAuthKey from './dialogs/new';
import TableList from '~/components/TableList';
import type { PreAuthKey, User } from '~/types';
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 AddPreAuthKey from './dialogs/new';
export async function action({ request }: ActionFunctionArgs) {
const session = await getSession(request.headers.get('Cookie'));
@ -156,6 +153,7 @@ export default function Page() {
return true;
});
// TODO: Fix the selects
return (
<div className="flex flex-col w-2/3">
<p className="mb-8 text-md">
@ -202,13 +200,13 @@ export default function Page() {
<Select
label="Filter by status"
placeholder="Select a status"
state={[status, setStatus]}
defaultSelectedKey="Active"
>
<Select.Item id="All">All</Select.Item>
<Select.Item id="Active">Active</Select.Item>
<Select.Item id="Used/Expired">Used/Expired</Select.Item>
<Select.Item id="Reusable">Reusable</Select.Item>
<Select.Item id="Ephemeral">Ephemeral</Select.Item>
<Select.Item key="All">All</Select.Item>
<Select.Item>Active</Select.Item>
<Select.Item>Used/Expired</Select.Item>
<Select.Item>Reusable</Select.Item>
<Select.Item>Ephemeral</Select.Item>
</Select>
</div>
</div>

View File

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