feat: add functional machine overview page and fix types
This commit is contained in:
parent
c08203cc76
commit
3ffbabd7fc
12 changed files with 458 additions and 289 deletions
|
|
@ -1,54 +1,62 @@
|
|||
import { type FetcherWithComponents } from '@remix-run/react'
|
||||
import { Form, useSubmit } 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>>];
|
||||
interface DeleteProps {
|
||||
readonly machine: Machine
|
||||
readonly state: [boolean, Dispatch<SetStateAction<boolean>>]
|
||||
}
|
||||
|
||||
export default function Delete({ machine, fetcher, state }: DeleteProperties) {
|
||||
export default function Delete({ machine, state }: DeleteProps) {
|
||||
const submit = useSubmit()
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Panel control={state}>
|
||||
{close => (
|
||||
<>
|
||||
<Dialog.Title>
|
||||
Remove {machine.givenName}
|
||||
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 method='POST'>
|
||||
<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'>
|
||||
<Form
|
||||
method="POST"
|
||||
onSubmit={(e) => {
|
||||
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'
|
||||
variant="cancel"
|
||||
onPress={close}
|
||||
>
|
||||
Cancel
|
||||
</Dialog.Action>
|
||||
<Dialog.Action
|
||||
variant='confirm'
|
||||
variant="confirm"
|
||||
className={cn(
|
||||
'bg-red-500 hover:border-red-700',
|
||||
'dark:bg-red-600 dark:hover:border-red-700',
|
||||
'pressed:bg-red-600 hover:bg-red-600',
|
||||
'text-white dark:text-white'
|
||||
'text-white dark:text-white',
|
||||
)}
|
||||
onPress={close}
|
||||
>
|
||||
Remove
|
||||
</Dialog.Action>
|
||||
</div>
|
||||
</fetcher.Form>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
|
|
|
|||
|
|
@ -1,54 +1,62 @@
|
|||
import { type FetcherWithComponents } from '@remix-run/react'
|
||||
import { Form, useSubmit } 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>>];
|
||||
interface ExpireProps {
|
||||
readonly machine: Machine
|
||||
readonly state: [boolean, Dispatch<SetStateAction<boolean>>]
|
||||
}
|
||||
|
||||
export default function Expire({ machine, fetcher, state }: DeleteProperties) {
|
||||
export default function Expire({ machine, state }: ExpireProps) {
|
||||
const submit = useSubmit()
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Panel control={state}>
|
||||
{close => (
|
||||
<>
|
||||
<Dialog.Title>
|
||||
Expire {machine.givenName}
|
||||
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.
|
||||
</Dialog.Text>
|
||||
<fetcher.Form method='POST'>
|
||||
<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'>
|
||||
<Form
|
||||
method="POST"
|
||||
onSubmit={(e) => {
|
||||
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'
|
||||
variant="cancel"
|
||||
onPress={close}
|
||||
>
|
||||
Cancel
|
||||
</Dialog.Action>
|
||||
<Dialog.Action
|
||||
variant='confirm'
|
||||
variant="confirm"
|
||||
className={cn(
|
||||
'bg-red-500 hover:border-red-700',
|
||||
'dark:bg-red-600 dark:hover:border-red-700',
|
||||
'pressed:bg-red-600 hover:bg-red-600',
|
||||
'text-white dark:text-white'
|
||||
'text-white dark:text-white',
|
||||
)}
|
||||
onPress={close}
|
||||
>
|
||||
Expire
|
||||
</Dialog.Action>
|
||||
</div>
|
||||
</fetcher.Form>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type FetcherWithComponents } from '@remix-run/react'
|
||||
import { Form, useSubmit } from '@remix-run/react'
|
||||
import { type Dispatch, type SetStateAction, useState } from 'react'
|
||||
|
||||
import Code from '~/components/Code'
|
||||
|
|
@ -6,15 +6,15 @@ import Dialog from '~/components/Dialog'
|
|||
import TextField from '~/components/TextField'
|
||||
import { type Machine } from '~/types'
|
||||
|
||||
type RenameProperties = {
|
||||
readonly machine: Machine;
|
||||
readonly fetcher: FetcherWithComponents<unknown>;
|
||||
readonly state: [boolean, Dispatch<SetStateAction<boolean>>];
|
||||
readonly magic?: string;
|
||||
interface RenameProps {
|
||||
readonly machine: Machine
|
||||
readonly state: [boolean, Dispatch<SetStateAction<boolean>>]
|
||||
readonly magic?: string
|
||||
}
|
||||
|
||||
export default function Rename({ machine, fetcher, state, magic }: RenameProperties) {
|
||||
export default function Rename({ machine, state, magic }: RenameProps) {
|
||||
const [name, setName] = useState(machine.givenName)
|
||||
const submit = useSubmit()
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
|
|
@ -22,65 +22,76 @@ export default function Rename({ machine, fetcher, state, magic }: RenamePropert
|
|||
{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.
|
||||
</Dialog.Text>
|
||||
<fetcher.Form method='POST'>
|
||||
<input type='hidden' name='_method' value='rename'/>
|
||||
<input type='hidden' name='id' value={machine.id}/>
|
||||
<Form
|
||||
method="POST"
|
||||
onSubmit={(e) => {
|
||||
submit(e.currentTarget)
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="_method" value="rename" />
|
||||
<input type="hidden" name="id" value={machine.id} />
|
||||
<TextField
|
||||
label='Machine name'
|
||||
placeholder='Machine name'
|
||||
name='name'
|
||||
label="Machine name"
|
||||
placeholder="Machine name"
|
||||
name="name"
|
||||
state={[name, setName]}
|
||||
className='my-2'
|
||||
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'>
|
||||
{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'
|
||||
variant="cancel"
|
||||
onPress={close}
|
||||
>
|
||||
Cancel
|
||||
</Dialog.Action>
|
||||
<Dialog.Action
|
||||
variant='confirm'
|
||||
variant="confirm"
|
||||
onPress={close}
|
||||
>
|
||||
Rename
|
||||
</Dialog.Action>
|
||||
</div>
|
||||
</fetcher.Form>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type FetcherWithComponents } from '@remix-run/react'
|
||||
import { useFetcher } from '@remix-run/react'
|
||||
import { type Dispatch, type SetStateAction } from 'react'
|
||||
|
||||
import Dialog from '~/components/Dialog'
|
||||
|
|
@ -6,22 +6,25 @@ import Switch from '~/components/Switch'
|
|||
import { type Machine, type Route } from '~/types'
|
||||
import { cn } from '~/utils/cn'
|
||||
|
||||
type RoutesProperties = {
|
||||
readonly machine: Machine;
|
||||
readonly routes: Route[];
|
||||
readonly fetcher: FetcherWithComponents<unknown>;
|
||||
readonly state: [boolean, Dispatch<SetStateAction<boolean>>];
|
||||
interface RoutesProps {
|
||||
readonly machine: Machine
|
||||
readonly routes: Route[]
|
||||
readonly state: [boolean, Dispatch<SetStateAction<boolean>>]
|
||||
}
|
||||
|
||||
// TODO: Support deleting routes
|
||||
export default function Routes({ machine, routes, fetcher, state }: RoutesProperties) {
|
||||
export default function Routes({ machine, routes, state }: RoutesProps) {
|
||||
const fetcher = useFetcher()
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Panel control={state}>
|
||||
{close => (
|
||||
<>
|
||||
<Dialog.Title>
|
||||
Edit route settings of {machine.givenName}
|
||||
Edit route settings of
|
||||
{' '}
|
||||
{machine.givenName}
|
||||
</Dialog.Title>
|
||||
<Dialog.Text>
|
||||
Connect to devices you can't install Tailscale on
|
||||
|
|
@ -30,28 +33,30 @@ export default function Routes({ machine, routes, fetcher, state }: RoutesProper
|
|||
<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'
|
||||
'border border-zinc-200 dark:border-zinc-700',
|
||||
)}
|
||||
>
|
||||
{routes.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}
|
||||
{routes.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}
|
||||
{routes.map(route => (
|
||||
<div
|
||||
key={route.node.id}
|
||||
className={cn(
|
||||
'flex py-2 px-4 bg-ui-100 dark:bg-ui-800',
|
||||
'items-center justify-between'
|
||||
'items-center justify-between',
|
||||
)}
|
||||
>
|
||||
<p>
|
||||
|
|
@ -59,8 +64,8 @@ export default function Routes({ machine, routes, fetcher, state }: RoutesProper
|
|||
</p>
|
||||
<Switch
|
||||
defaultSelected={route.enabled}
|
||||
label='Enabled'
|
||||
onChange={checked => {
|
||||
label="Enabled"
|
||||
onChange={(checked) => {
|
||||
const form = new FormData()
|
||||
form.set('id', machine.id)
|
||||
form.set('_method', 'routes')
|
||||
|
|
@ -68,16 +73,16 @@ export default function Routes({ machine, routes, fetcher, state }: RoutesProper
|
|||
|
||||
form.set('enabled', String(checked))
|
||||
fetcher.submit(form, {
|
||||
method: 'POST'
|
||||
method: 'POST',
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className='mt-6 flex justify-end gap-2 mt-6'>
|
||||
<div className="mt-6 flex justify-end gap-2 mt-6">
|
||||
<Dialog.Action
|
||||
variant='cancel'
|
||||
variant="cancel"
|
||||
isDisabled={fetcher.state === 'submitting'}
|
||||
onPress={close}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue