chore: switch to react-router v7

This commit is contained in:
Aarnav Tale 2024-12-31 10:30:14 +05:30
parent 39504e2487
commit aa9872a45b
No known key found for this signature in database
101 changed files with 3825 additions and 6796 deletions

View file

@ -1,46 +1,39 @@
import { Form, useSubmit } from '@remix-run/react'
import { type Dispatch, type SetStateAction } from 'react'
import { Form, useSubmit } from 'react-router';
import { type Dispatch, type SetStateAction } from 'react';
import Dialog from '~/components/Dialog'
import { type Machine } from '~/types'
import { cn } from '~/utils/cn'
import Dialog from '~/components/Dialog';
import { type Machine } from '~/types';
import { cn } from '~/utils/cn';
interface DeleteProps {
readonly machine: Machine
readonly state: [boolean, Dispatch<SetStateAction<boolean>>]
readonly machine: Machine;
readonly state: [boolean, Dispatch<SetStateAction<boolean>>];
}
export default function Delete({ machine, state }: DeleteProps) {
const submit = useSubmit()
const submit = useSubmit();
return (
<Dialog>
<Dialog.Panel control={state}>
{close => (
{(close) => (
<>
<Dialog.Title>
Remove
{' '}
{machine.givenName}
</Dialog.Title>
<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.
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>
<Form
method="POST"
onSubmit={(e) => {
submit(e.currentTarget)
submit(e.currentTarget);
}}
>
<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}
>
<Dialog.Action variant="cancel" onPress={close}>
Cancel
</Dialog.Action>
<Dialog.Action
@ -61,5 +54,5 @@ export default function Delete({ machine, state }: DeleteProps) {
)}
</Dialog.Panel>
</Dialog>
)
);
}

View file

@ -1,46 +1,38 @@
import { Form, useSubmit } from '@remix-run/react'
import { type Dispatch, type SetStateAction } from 'react'
import { Form, useSubmit } from 'react-router';
import { type Dispatch, type SetStateAction } from 'react';
import Dialog from '~/components/Dialog'
import { type Machine } from '~/types'
import { cn } from '~/utils/cn'
import Dialog from '~/components/Dialog';
import { type Machine } from '~/types';
import { cn } from '~/utils/cn';
interface ExpireProps {
readonly machine: Machine
readonly state: [boolean, Dispatch<SetStateAction<boolean>>]
readonly machine: Machine;
readonly state: [boolean, Dispatch<SetStateAction<boolean>>];
}
export default function Expire({ machine, state }: ExpireProps) {
const submit = useSubmit()
const submit = useSubmit();
return (
<Dialog>
<Dialog.Panel control={state}>
{close => (
{(close) => (
<>
<Dialog.Title>
Expire
{' '}
{machine.givenName}
</Dialog.Title>
<Dialog.Title>Expire {machine.givenName}</Dialog.Title>
<Dialog.Text>
This will disconnect the machine from your Tailnet.
In order to reconnect, you will need to re-authenticate
from the device.
This will disconnect the machine from your Tailnet. In order to
reconnect, you will need to re-authenticate from the device.
</Dialog.Text>
<Form
method="POST"
onSubmit={(e) => {
submit(e.currentTarget)
submit(e.currentTarget);
}}
>
<input type="hidden" name="_method" value="expire" />
<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}
>
<Dialog.Action variant="cancel" onPress={close}>
Cancel
</Dialog.Action>
<Dialog.Action
@ -61,5 +53,5 @@ export default function Expire({ machine, state }: ExpireProps) {
)}
</Dialog.Panel>
</Dialog>
)
);
}

View file

@ -1,39 +1,35 @@
import { Form, useSubmit } from '@remix-run/react'
import { type Dispatch, type SetStateAction, useState } from 'react'
import { Form, useSubmit } from 'react-router';
import { type Dispatch, type SetStateAction, useState } from 'react';
import Code from '~/components/Code'
import Dialog from '~/components/Dialog'
import Select from '~/components/Select'
import { type Machine, User } from '~/types'
import Code from '~/components/Code';
import Dialog from '~/components/Dialog';
import Select from '~/components/Select';
import { type Machine, User } from '~/types';
interface MoveProps {
readonly machine: Machine
readonly users: User[]
readonly state: [boolean, Dispatch<SetStateAction<boolean>>]
readonly magic?: string
readonly machine: Machine;
readonly users: User[];
readonly state: [boolean, Dispatch<SetStateAction<boolean>>];
readonly magic?: string;
}
export default function Move({ machine, state, magic, users }: MoveProps) {
const [owner, setOwner] = useState(machine.user.name)
const submit = useSubmit()
const [owner, setOwner] = useState(machine.user.name);
const submit = useSubmit();
return (
<Dialog>
<Dialog.Panel control={state}>
{close => (
{(close) => (
<>
<Dialog.Title>
Change the owner of
{' '}
{machine.givenName}
</Dialog.Title>
<Dialog.Title>Change the owner of {machine.givenName}</Dialog.Title>
<Dialog.Text>
The owner of the machine is the user associated with it.
</Dialog.Text>
<Form
method="POST"
onSubmit={(e) => {
submit(e.currentTarget)
submit(e.currentTarget);
}}
>
<input type="hidden" name="_method" value="move" />
@ -44,37 +40,26 @@ export default function Move({ machine, state, magic, users }: MoveProps) {
placeholder="Select a user"
state={[owner, setOwner]}
>
{users.map(user => (
{users.map((user) => (
<Select.Item key={user.id} id={user.name}>
{user.name}
</Select.Item>
))}
</Select>
{magic
? (
<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}
.
{magic}
</Code>
.
</p>
)
: undefined}
{magic ? (
<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}.{magic}
</Code>
.
</p>
) : undefined}
<div className="mt-6 flex justify-end gap-2 mt-6">
<Dialog.Action
variant="cancel"
onPress={close}
>
<Dialog.Action variant="cancel" onPress={close}>
Cancel
</Dialog.Action>
<Dialog.Action
variant="confirm"
onPress={close}
>
<Dialog.Action variant="confirm" onPress={close}>
Change owner
</Dialog.Action>
</div>
@ -83,5 +68,5 @@ export default function Move({ machine, state, magic, users }: MoveProps) {
)}
</Dialog.Panel>
</Dialog>
)
);
}

View file

@ -1,74 +1,73 @@
import { Form, useFetcher, Link } from '@remix-run/react'
import { Dispatch, SetStateAction, useState, useEffect } from 'react'
import { PlusIcon, ServerIcon, KeyIcon } from '@primer/octicons-react'
import { cn } from '~/utils/cn'
import { Form, useFetcher, Link } from 'react-router';
import { Dispatch, SetStateAction, useState, useEffect } from 'react';
import { PlusIcon, ServerIcon, KeyIcon } from '@primer/octicons-react';
import { cn } from '~/utils/cn';
import Code from '~/components/Code'
import Dialog from '~/components/Dialog'
import TextField from '~/components/TextField'
import Select from '~/components/Select'
import Menu from '~/components/Menu'
import Spinner from '~/components/Spinner'
import { toast } from '~/components/Toaster'
import { Machine, User } from '~/types'
import Code from '~/components/Code';
import Dialog from '~/components/Dialog';
import TextField from '~/components/TextField';
import Select from '~/components/Select';
import Menu from '~/components/Menu';
import Spinner from '~/components/Spinner';
import { toast } from '~/components/Toaster';
import { Machine, User } from '~/types';
export interface NewProps {
server: string
users: User[]
server: string;
users: User[];
}
export default function New(data: NewProps) {
const fetcher = useFetcher<{ success?: boolean }>()
const mkeyState = useState(false)
const [mkey, setMkey] = useState('')
const [user, setUser] = useState('')
const [toasted, setToasted] = useState(false)
const fetcher = useFetcher<{ success?: boolean }>();
const mkeyState = useState(false);
const [mkey, setMkey] = useState('');
const [user, setUser] = useState('');
const [toasted, setToasted] = useState(false);
useEffect(() => {
if (!fetcher.data || toasted) {
return
return;
}
if (fetcher.data.success) {
toast('Registered new machine')
toast('Registered new machine');
} else {
toast('Failed to register machine due to an invalid key')
toast('Failed to register machine due to an invalid key');
}
setToasted(true)
}, [fetcher.data, toasted, mkey])
setToasted(true);
}, [fetcher.data, toasted, mkey]);
return (
<>
<Dialog>
<Dialog.Panel control={mkeyState}>
{close => (
{(close) => (
<>
<Dialog.Title>
Register Machine Key
</Dialog.Title>
<Dialog.Text className='mb-4'>
The machine key is given when you run
{' '}
<Dialog.Title>Register Machine Key</Dialog.Title>
<Dialog.Text className="mb-4">
The machine key is given when you run{' '}
<Code isCopyable>
tailscale up --login-server=
{data.server}
</Code>
{' '}
</Code>{' '}
on your device.
</Dialog.Text>
<fetcher.Form method="POST" onSubmit={e => {
fetcher.submit(e.currentTarget)
close()
}}>
<fetcher.Form
method="POST"
onSubmit={(e) => {
fetcher.submit(e.currentTarget);
close();
}}
>
<input type="hidden" name="_method" value="register" />
<input type="hidden" name="id" value="_" />
<TextField
label='Machine Key'
placeholder='mkey:ff.....'
label="Machine Key"
placeholder="mkey:ff....."
name="mkey"
state={[mkey, setMkey]}
className='my-2 font-mono'
className="my-2 font-mono"
/>
<Select
label="Owner"
@ -76,28 +75,25 @@ export default function New(data: NewProps) {
placeholder="Select a user"
state={[user, setUser]}
>
{data.users.map(user => (
{data.users.map((user) => (
<Select.Item key={user.id} id={user.name}>
{user.name}
</Select.Item>
))}
</Select>
<div className='mt-6 flex justify-end gap-2 mt-6'>
<Dialog.Action
variant="cancel"
onPress={close}
>
<div className="mt-6 flex justify-end gap-2 mt-6">
<Dialog.Action variant="cancel" onPress={close}>
Cancel
</Dialog.Action>
<Dialog.Action
variant="confirm"
isDisabled={!mkey || !mkey.trim().startsWith('mkey:') || !user}
isDisabled={
!mkey || !mkey.trim().startsWith('mkey:') || !user
}
>
{fetcher.state === 'idle'
? undefined
: (
<Spinner className="w-3 h-3" />
)}
{fetcher.state === 'idle' ? undefined : (
<Spinner className="w-3 h-3" />
)}
Register
</Dialog.Action>
</div>
@ -118,17 +114,17 @@ export default function New(data: NewProps) {
</Menu.Button>
<Menu.Items>
<Menu.ItemButton control={mkeyState}>
<ServerIcon className='w-4 h-4 mr-2'/>
<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'/>
<KeyIcon className="w-4 h-4 mr-2" />
Generate Pre-auth Key
</Link>
</Menu.ItemButton>
</Menu.Items>
</Menu>
</>
)
);
}

View file

@ -1,39 +1,37 @@
import { Form, useSubmit } from '@remix-run/react'
import { type Dispatch, type SetStateAction, useState } from 'react'
import { Form, useSubmit } from 'react-router';
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 Code from '~/components/Code';
import Dialog from '~/components/Dialog';
import TextField from '~/components/TextField';
import { type Machine } from '~/types';
interface RenameProps {
readonly machine: Machine
readonly state: [boolean, Dispatch<SetStateAction<boolean>>]
readonly magic?: string
readonly machine: Machine;
readonly state: [boolean, Dispatch<SetStateAction<boolean>>];
readonly magic?: string;
}
export default function Rename({ machine, state, magic }: RenameProps) {
const [name, setName] = useState(machine.givenName)
const submit = useSubmit()
const [name, setName] = useState(machine.givenName);
const submit = useSubmit();
return (
<Dialog>
<Dialog.Panel control={state}>
{close => (
{(close) => (
<>
<Dialog.Title>
Edit machine name for
{' '}
{machine.givenName}
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.
This name is shown in the admin panel, in Tailscale clients, and
used when generating MagicDNS names.
</Dialog.Text>
<Form
method="POST"
onSubmit={(e) => {
submit(e.currentTarget)
submit(e.currentTarget);
}}
>
<input type="hidden" name="_method" value="rename" />
@ -45,49 +43,30 @@ export default function Rename({ machine, state, magic }: RenameProps) {
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}
{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}
>
<Dialog.Action variant="cancel" onPress={close}>
Cancel
</Dialog.Action>
<Dialog.Action
variant="confirm"
onPress={close}
>
<Dialog.Action variant="confirm" onPress={close}>
Rename
</Dialog.Action>
</div>
@ -96,5 +75,5 @@ export default function Rename({ machine, state, magic }: RenameProps) {
)}
</Dialog.Panel>
</Dialog>
)
);
}

View file

@ -1,55 +1,53 @@
import { useFetcher } from '@remix-run/react'
import { Dispatch, SetStateAction, useMemo } from 'react'
import { useFetcher } from 'react-router';
import { Dispatch, SetStateAction, useMemo } from 'react';
import Dialog from '~/components/Dialog'
import Switch from '~/components/Switch'
import Link from '~/components/Link'
import { Machine, Route } from '~/types'
import { cn } from '~/utils/cn'
import Dialog from '~/components/Dialog';
import Switch from '~/components/Switch';
import Link from '~/components/Link';
import { Machine, Route } from '~/types';
import { cn } from '~/utils/cn';
interface RoutesProps {
readonly machine: Machine
readonly routes: Route[]
readonly state: [boolean, Dispatch<SetStateAction<boolean>>]
readonly machine: Machine;
readonly routes: Route[];
readonly state: [boolean, Dispatch<SetStateAction<boolean>>];
}
// TODO: Support deleting routes
export default function Routes({ machine, routes, state }: RoutesProps) {
const fetcher = useFetcher()
const fetcher = useFetcher();
// This is much easier with Object.groupBy but it's too new for us
const { exit, subnet } = routes.reduce((acc, route) => {
if (route.prefix === '::/0' || route.prefix === '0.0.0.0/0') {
acc.exit.push(route)
return acc
}
const { exit, subnet } = routes.reduce(
(acc, route) => {
if (route.prefix === '::/0' || route.prefix === '0.0.0.0/0') {
acc.exit.push(route);
return acc;
}
acc.subnet.push(route)
return acc
}, { exit: [], subnet: [] })
acc.subnet.push(route);
return acc;
},
{ exit: [], subnet: [] },
);
const exitEnabled = useMemo(() => {
if (exit.length !== 2) return false
return exit[0].enabled && exit[1].enabled
}, [exit])
if (exit.length !== 2) return false;
return exit[0].enabled && exit[1].enabled;
}, [exit]);
return (
<Dialog>
<Dialog.Panel control={state}>
{close => (
{(close) => (
<>
<Dialog.Title>
Edit route settings of
{' '}
{machine.givenName}
Edit route settings of {machine.givenName}
</Dialog.Title>
<Dialog.Text className="font-bold">
Subnet routes
</Dialog.Text>
<Dialog.Text className="font-bold">Subnet routes</Dialog.Text>
<Dialog.Text>
Connect to devices you can&apos;t install Tailscale on
by advertising IP ranges as subnet routes.
{' '}
Connect to devices you can&apos;t install Tailscale on by
advertising IP ranges as subnet routes.{' '}
<Link
to="https://tailscale.com/kb/1019/subnets"
name="Tailscale Subnets Documentation"
@ -57,28 +55,25 @@ export default function Routes({ machine, routes, state }: RoutesProps) {
Learn More
</Link>
</Dialog.Text>
<div className={cn(
'rounded-lg overflow-y-auto my-2',
'divide-y divide-zinc-200 dark:divide-zinc-700 align-top',
'border border-zinc-200 dark:border-zinc-700',
)}
<div
className={cn(
'rounded-lg overflow-y-auto my-2',
'divide-y divide-zinc-200 dark:divide-zinc-700 align-top',
'border border-zinc-200 dark:border-zinc-700',
)}
>
{subnet.length === 0
? (
<div
className={cn(
'flex py-4 px-4 bg-ui-100 dark:bg-ui-800',
'items-center justify-center',
'text-ui-600 dark:text-ui-300',
)}
>
<p>
No routes are advertised on this machine.
</p>
</div>
)
: undefined}
{subnet.map(route => (
{subnet.length === 0 ? (
<div
className={cn(
'flex py-4 px-4 bg-ui-100 dark:bg-ui-800',
'items-center justify-center',
'text-ui-600 dark:text-ui-300',
)}
>
<p>No routes are advertised on this machine.</p>
</div>
) : undefined}
{subnet.map((route) => (
<div
key={route.id}
className={cn(
@ -86,33 +81,28 @@ export default function Routes({ machine, routes, state }: RoutesProps) {
'items-center justify-between',
)}
>
<p>
{route.prefix}
</p>
<p>{route.prefix}</p>
<Switch
defaultSelected={route.enabled}
label="Enabled"
onChange={(checked) => {
const form = new FormData()
form.set('id', machine.id)
form.set('_method', 'routes')
form.set('route', route.id)
const form = new FormData();
form.set('id', machine.id);
form.set('_method', 'routes');
form.set('route', route.id);
form.set('enabled', String(checked))
form.set('enabled', String(checked));
fetcher.submit(form, {
method: 'POST',
})
});
}}
/>
</div>
))}
</div>
<Dialog.Text className="font-bold mt-8">
Exit nodes
</Dialog.Text>
<Dialog.Text className="font-bold mt-8">Exit nodes</Dialog.Text>
<Dialog.Text>
Allow your network to route internet traffic through this machine.
{' '}
Allow your network to route internet traffic through this machine.{' '}
<Link
to="https://tailscale.com/kb/1103/exit-nodes"
name="Tailscale Exit-node Documentation"
@ -120,52 +110,51 @@ export default function Routes({ machine, routes, state }: RoutesProps) {
Learn More
</Link>
</Dialog.Text>
<div className={cn(
'rounded-lg overflow-y-auto my-2',
'divide-y divide-zinc-200 dark:divide-zinc-700 align-top',
'border border-zinc-200 dark:border-zinc-700',
)}
<div
className={cn(
'rounded-lg overflow-y-auto my-2',
'divide-y divide-zinc-200 dark:divide-zinc-700 align-top',
'border border-zinc-200 dark:border-zinc-700',
)}
>
{exit.length === 0
? (
<div
className={cn(
'flex py-4 px-4 bg-ui-100 dark:bg-ui-800',
'items-center justify-center',
'text-ui-600 dark:text-ui-300',
)}
>
<p>
This machine is not an exit node.
</p>
</div>
) : (
<div
className={cn(
'flex py-2 px-4 bg-ui-100 dark:bg-ui-800',
'items-center justify-between',
)}
>
<p>
Use as exit node
</p>
<Switch
defaultSelected={exitEnabled}
label="Enabled"
onChange={(checked) => {
const form = new FormData()
form.set('id', machine.id)
form.set('_method', 'exit-node')
form.set('routes', exit.map(route => route.id).join(','))
{exit.length === 0 ? (
<div
className={cn(
'flex py-4 px-4 bg-ui-100 dark:bg-ui-800',
'items-center justify-center',
'text-ui-600 dark:text-ui-300',
)}
>
<p>This machine is not an exit node.</p>
</div>
) : (
<div
className={cn(
'flex py-2 px-4 bg-ui-100 dark:bg-ui-800',
'items-center justify-between',
)}
>
<p>Use as exit node</p>
<Switch
defaultSelected={exitEnabled}
label="Enabled"
onChange={(checked) => {
const form = new FormData();
form.set('id', machine.id);
form.set('_method', 'exit-node');
form.set(
'routes',
exit.map((route) => route.id).join(','),
);
form.set('enabled', String(checked))
fetcher.submit(form, {
method: 'POST',
})
}}
/>
</div>
)}
form.set('enabled', String(checked));
fetcher.submit(form, {
method: 'POST',
});
}}
/>
</div>
)}
</div>
<div className="mt-6 flex justify-end gap-2 mt-6">
<Dialog.Action
@ -180,5 +169,5 @@ export default function Routes({ machine, routes, state }: RoutesProps) {
)}
</Dialog.Panel>
</Dialog>
)
);
}

View file

@ -1,51 +1,44 @@
import { PlusIcon, XIcon } from '@primer/octicons-react'
import { Form, useSubmit } from '@remix-run/react'
import { Dispatch, SetStateAction, useState } from 'react'
import { Button, Input } from 'react-aria-components'
import { PlusIcon, XIcon } from '@primer/octicons-react';
import { Form, useSubmit } from 'react-router';
import { Dispatch, SetStateAction, useState } from 'react';
import { Button, Input } from 'react-aria-components';
import Dialog from '~/components/Dialog'
import Link from '~/components/Link'
import { Machine } from '~/types'
import { cn } from '~/utils/cn'
import Dialog from '~/components/Dialog';
import Link from '~/components/Link';
import { Machine } from '~/types';
import { cn } from '~/utils/cn';
interface TagsProps {
readonly machine: Machine
readonly state: [boolean, Dispatch<SetStateAction<boolean>>]
readonly machine: Machine;
readonly state: [boolean, Dispatch<SetStateAction<boolean>>];
}
export default function Tags({ machine, state }: TagsProps) {
const [tags, setTags] = useState(machine.forcedTags)
const [tag, setTag] = useState('')
const submit = useSubmit()
const [tags, setTags] = useState(machine.forcedTags);
const [tag, setTag] = useState('');
const submit = useSubmit();
return (
<Dialog>
<Dialog.Panel control={state}>
{close => (
{(close) => (
<>
<Dialog.Title>
Edit ACL tags for
{' '}
{machine.givenName}
</Dialog.Title>
<Dialog.Title>Edit ACL tags for {machine.givenName}</Dialog.Title>
<Dialog.Text>
ACL tags can be used to reference machines in your ACL policies.
See the
{' '}
See the{' '}
<Link
to="https://tailscale.com/kb/1068/acl-tags"
name="Tailscale documentation"
>
Tailscale documentation
</Link>
{' '}
</Link>{' '}
for more information.
</Dialog.Text>
<Form
method="POST"
onSubmit={(e) => {
submit(e.currentTarget)
submit(e.currentTarget);
}}
>
<input type="hidden" name="_method" value="tags" />
@ -58,21 +51,18 @@ export default function Tags({ machine, state }: TagsProps) {
)}
>
<div className="divide-y divide-ui-200 dark:divide-ui-600">
{tags.length === 0
? (
<div
className={cn(
'flex py-4 px-4 bg-ui-100 dark:bg-ui-800',
'items-center justify-center rounded-t-lg',
'text-ui-600 dark:text-ui-300',
)}
>
<p>
No tags are set on this machine.
</p>
</div>
)
: tags.map(item => (
{tags.length === 0 ? (
<div
className={cn(
'flex py-4 px-4 bg-ui-100 dark:bg-ui-800',
'items-center justify-center rounded-t-lg',
'text-ui-600 dark:text-ui-300',
)}
>
<p>No tags are set on this machine.</p>
</div>
) : (
tags.map((item) => (
<div
key={item}
id={item}
@ -86,13 +76,14 @@ export default function Tags({ machine, state }: TagsProps) {
<Button
className="rounded-full p-0 w-6 h-6"
onPress={() => {
setTags(tags.filter(tag => tag !== item))
setTags(tags.filter((tag) => tag !== item));
}}
>
<XIcon className="w-4 h-4" />
</Button>
</div>
))}
))
)}
</div>
<div
className={cn(
@ -101,8 +92,9 @@ export default function Tags({ machine, state }: TagsProps) {
'rounded-b-lg justify-between items-center',
'dark:bg-ui-800 dark:text-ui-300',
'focus-within:ring-2 focus-within:ring-blue-600',
tag.length > 0 && !tag.startsWith('tag:')
&& 'outline outline-red-500',
tag.length > 0 &&
!tag.startsWith('tag:') &&
'outline outline-red-500',
)}
>
<Input
@ -115,23 +107,23 @@ export default function Tags({ machine, state }: TagsProps) {
)}
value={tag}
onChange={(e) => {
setTag(e.currentTarget.value)
setTag(e.currentTarget.value);
}}
/>
<Button
className={cn(
'rounded-lg p-0 h-6 w-6',
!tag.startsWith('tag:')
&& 'opacity-50 cursor-not-allowed',
!tag.startsWith('tag:') &&
'opacity-50 cursor-not-allowed',
)}
isDisabled={
tag.length === 0
|| !tag.startsWith('tag:')
|| tags.includes(tag)
tag.length === 0 ||
!tag.startsWith('tag:') ||
tags.includes(tag)
}
onPress={() => {
setTags([...tags, tag])
setTag('')
setTags([...tags, tag]);
setTag('');
}}
>
<PlusIcon className="w-4 h-4" />
@ -139,16 +131,10 @@ export default function Tags({ machine, state }: TagsProps) {
</div>
</div>
<div className="mt-6 flex justify-end gap-2 mt-6">
<Dialog.Action
variant="cancel"
onPress={close}
>
<Dialog.Action variant="cancel" onPress={close}>
Cancel
</Dialog.Action>
<Dialog.Action
variant="confirm"
onPress={close}
>
<Dialog.Action variant="confirm" onPress={close}>
Save
</Dialog.Action>
</div>
@ -157,5 +143,5 @@ export default function Tags({ machine, state }: TagsProps) {
)}
</Dialog.Panel>
</Dialog>
)
);
}