feat: switch to react aria and replace many components

This commit is contained in:
Aarnav Tale
2024-04-29 20:31:29 -04:00
parent cbecf85979
commit 3abd617a90
11 changed files with 536 additions and 298 deletions
@@ -0,0 +1,55 @@
import { type FetcherWithComponents } from '@remix-run/react'
import { type Dispatch, type SetStateAction } from 'react'
import Dialog from '~/components/Dialog'
import { type Machine } from '~/types'
import { cn } from '~/utils/cn'
type DeleteProperties = {
readonly machine: Machine;
readonly fetcher: FetcherWithComponents<unknown>;
readonly state: [boolean, Dispatch<SetStateAction<boolean>>];
}
export default function Delete({ machine, fetcher, state }: DeleteProperties) {
return (
<Dialog>
<Dialog.Panel control={state}>
{close => (
<>
<Dialog.Title>
Remove {machine.givenName}
</Dialog.Title>
<Dialog.Text>
This machine will be permanently removed from
your network. To re-add it, you will need to
reauthenticate to your tailnet from the device.
</Dialog.Text>
<fetcher.Form>
<input type='hidden' name='_method' value='delete'/>
<input type='hidden' name='id' value={machine.id}/>
<div className='mt-6 flex justify-end gap-2 mt-6'>
<Dialog.Action
variant='cancel'
onPress={close}
>
Cancel
</Dialog.Action>
<Dialog.Action
variant='confirm'
className={cn(
'bg-red-500 hover:border-red-700',
'pressed:bg-red-600'
)}
onPress={close}
>
Remove
</Dialog.Action>
</div>
</fetcher.Form>
</>
)}
</Dialog.Panel>
</Dialog>
)
}
@@ -0,0 +1,90 @@
import { type FetcherWithComponents } from '@remix-run/react'
import { type Dispatch, type SetStateAction, useState } from 'react'
import Code from '~/components/Code'
import Dialog from '~/components/Dialog'
import TextField from '~/components/TextField'
import { type Machine } from '~/types'
import { cn } from '~/utils/cn'
type RenameProperties = {
readonly machine: Machine;
readonly fetcher: FetcherWithComponents<unknown>;
readonly state: [boolean, Dispatch<SetStateAction<boolean>>];
readonly magic?: string;
}
export default function Rename({ machine, fetcher, state, magic }: RenameProperties) {
const [name, setName] = useState(machine.givenName)
return (
<Dialog>
<Dialog.Panel control={state}>
{close => (
<>
<Dialog.Title>
Edit machine name for {machine.givenName}
</Dialog.Title>
<Dialog.Text>
This name is shown in the admin panel, in Tailscale clients,
and used when generating MagicDNS names.
</Dialog.Text>
<fetcher.Form>
<input type='hidden' name='_method' value='rename'/>
<input type='hidden' name='id' value={machine.id}/>
<TextField
label='Machine name'
placeholder='Machine name'
name='name'
state={[name, setName]}
className='my-2'
/>
{magic ? (
name.length > 0 && name !== machine.givenName ? (
<p className='text-sm text-gray-500 dark:text-gray-300 leading-tight'>
This machine will be accessible by the hostname
{' '}
<Code className='text-sm'>
{name.toLowerCase().replaceAll(/\s+/g, '-')}
</Code>
{'. '}
The hostname
{' '}
<Code className='text-sm'>
{machine.givenName}
</Code>
{' '}
will no longer point to this machine.
</p>
) : (
<p className='text-sm text-gray-500 dark:text-gray-300 leading-tight'>
This machine is accessible by the hostname
{' '}
<Code className='text-sm'>
{machine.givenName}
</Code>
.
</p>
)
) : undefined}
<div className='mt-6 flex justify-end gap-2 mt-6'>
<Dialog.Action
variant='cancel'
onPress={close}
>
Cancel
</Dialog.Action>
<Dialog.Action
variant='confirm'
onPress={close}
>
Rename
</Dialog.Action>
</div>
</fetcher.Form>
</>
)}
</Dialog.Panel>
</Dialog>
)
}
+100 -124
View File
@@ -1,22 +1,29 @@
/* eslint-disable react/hook-use-state */
import { ChevronDownIcon, ClipboardIcon, EllipsisHorizontalIcon } from '@heroicons/react/24/outline'
import { type FetcherWithComponents, Link } from '@remix-run/react'
import clsx from 'clsx'
import { useState } from 'react'
import toast from 'react-hot-toast/headless'
import Dropdown from '~/components/Dropdown'
import type { OpenFunction } from '~/components/Modal'
import Dialog from '~/components/Dialog'
import Menu from '~/components/Menu'
import StatusCircle from '~/components/StatusCircle'
import { type Machine } from '~/types'
import { cn } from '~/utils/cn'
import Delete from './dialogs/delete'
import Rename from './dialogs/rename'
type MachineProperties = {
readonly machine: Machine;
readonly open: OpenFunction;
readonly fetcher: FetcherWithComponents<unknown>;
readonly magic?: string;
}
export default function MachineRow({ machine, open, fetcher, magic }: MachineProperties) {
export default function MachineRow({ machine, fetcher, magic }: MachineProperties) {
const tags = [...machine.forcedTags, ...machine.validTags]
const renameState = useState(false)
const removeState = useState(false)
return (
<tr
key={machine.id}
@@ -27,7 +34,7 @@ export default function MachineRow({ machine, open, fetcher, magic }: MachinePro
to={`/machines/${machine.id}`}
className='group/link h-full'
>
<p className={clsx(
<p className={cn(
'font-semibold leading-snug',
'group-hover/link:text-blue-600',
'group-hover/link:dark:text-blue-400'
@@ -35,14 +42,14 @@ export default function MachineRow({ machine, open, fetcher, magic }: MachinePro
>
{machine.givenName}
</p>
<p className='text-sm text-gray-400 font-mono'>
<p className='text-sm text-gray-500 dark:text-gray-300 font-mono'>
{machine.name}
</p>
<div className='flex gap-1 mt-1'>
{tags.map(tag => (
<span
key={tag}
className={clsx(
className={cn(
'text-xs rounded-sm px-1 py-0.5',
'bg-gray-100 dark:bg-zinc-700',
'text-gray-600 dark:text-gray-300'
@@ -57,50 +64,53 @@ export default function MachineRow({ machine, open, fetcher, magic }: MachinePro
<td className='py-2'>
<div className='flex items-center gap-x-1'>
{machine.ipAddresses[0]}
<Dropdown
width='w-max'
button={(
<Menu>
<Menu.Button>
<ChevronDownIcon className='w-4 h-4'/>
)}
>
{machine.ipAddresses.map(ip => (
<Dropdown.Item key={ip}>
<button
type='button'
className={clsx(
'flex items-center gap-x-1.5 text-sm',
'justify-between w-full'
)}
onClick={async () => {
await navigator.clipboard.writeText(ip)
toast('Copied IP address to clipboard')
}}
</Menu.Button>
<Menu.Items>
{machine.ipAddresses.map(ip => (
<Menu.Item
key={ip}
className='hover:bg-transparent'
>
{ip}
<ClipboardIcon className='w-3 h-3'/>
</button>
</Dropdown.Item>
))}
{magic ? (
<Dropdown.Item>
<button
type='button'
className={clsx(
'flex items-center gap-x-1.5 text-sm',
'justify-between w-full break-keep'
)}
onClick={async () => {
const ip = `${machine.givenName}.${machine.user.name}.${magic}`
await navigator.clipboard.writeText(ip)
toast('Copied hostname to clipboard')
}}
>
{machine.givenName}.{machine.user.name}.{magic}
<ClipboardIcon className='w-3 h-3'/>
</button>
</Dropdown.Item>
) : undefined}
</Dropdown>
<button
type='button'
className={cn(
'flex items-center gap-x-1.5 text-sm',
'justify-between w-full'
)}
onClick={async () => {
await navigator.clipboard.writeText(ip)
toast('Copied IP address to clipboard')
}}
>
{ip}
<ClipboardIcon className='w-3 h-3'/>
</button>
</Menu.Item>
))}
{magic ? (
<Menu.Item className='hover:bg-transparent'>
<button
type='button'
className={cn(
'flex items-center gap-x-1.5 text-sm',
'justify-between w-full break-keep'
)}
onClick={async () => {
const ip = `${machine.givenName}.${machine.user.name}.${magic}`
await navigator.clipboard.writeText(ip)
toast('Copied hostname to clipboard')
}}
>
{machine.givenName}.{machine.user.name}.{magic}
<ClipboardIcon className='w-3 h-3'/>
</button>
</Menu.Item>
) : undefined}
</Menu.Items>
</Menu>
</div>
</td>
<td className='py-2'>
@@ -118,86 +128,52 @@ export default function MachineRow({ machine, open, fetcher, magic }: MachinePro
</span>
</td>
<td className='py-2 pr-0.5'>
<div className={clsx(
'border border-transparent rounded-lg py-0.5 w-10',
'group-hover:border-gray-200 dark:group-hover:border-zinc-700'
)}
>
<Dropdown
className='left-1/4'
width='w-48'
button={(
<EllipsisHorizontalIcon className='w-5 h-5'/>
<Rename
machine={machine}
fetcher={fetcher}
state={renameState}
magic={magic}
/>
<Delete
machine={machine}
fetcher={fetcher}
state={removeState}
/>
<Menu>
<Menu.Button
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'
)}
>
<Dropdown.Item variant='static'>
<button
disabled
type='button'
className='text-left w-full opacity-50'
onClick={() => {
open()
}}
<EllipsisHorizontalIcon className='w-5'/>
</Menu.Button>
<Menu.Items>
<Menu.Item>
<Dialog.Button
className='h-full w-full text-left'
control={renameState}
>
Edit machine name
</button>
</Dropdown.Item>
<Dropdown.Item variant='static'>
<button
disabled
type='button'
className='text-left w-full opacity-50'
onClick={() => {
open()
}}
>
Edit route settings
</button>
</Dropdown.Item>
<Dropdown.Item variant='static'>
<button
disabled
type='button'
className='text-left w-full opacity-50'
onClick={() => {
open()
}}
>
Edit ACL tags
</button>
</Dropdown.Item>
<Dropdown.Item>
<button
type='button'
className='text-left text-red-700 w-full'
onClick={() => {
open({
title: 'Remove Machine',
description: [
'This action is irreversible and will disconnect the machine from the Headscale server.',
'All data associated with this machine including ACLs and tags will be lost.'
].join('\n'),
variant: 'danger',
buttonText: 'Remove',
onConfirm: () => {
fetcher.submit(
{
id: machine.id
},
{
method: 'DELETE',
encType: 'application/json'
}
)
}
})
}}
</Dialog.Button>
</Menu.Item>
<Menu.Item>
Edit route settings
</Menu.Item>
<Menu.Item>
Edit ACL tags
</Menu.Item>
<Menu.Item>
<Dialog.Button
className='w-full h-full text-left text-red-500 dark:text-red-400'
control={removeState}
>
Remove
</button>
</Dropdown.Item>
</Dropdown>
</div>
</Dialog.Button>
</Menu.Item>
</Menu.Items>
</Menu>
</td>
</tr>
)