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,32 +1,27 @@
import { useSubmit } from '@remix-run/react'
import { Button } from 'react-aria-components'
import { useSubmit } from 'react-router';
import { Button } from 'react-aria-components';
import Code from '~/components/Code'
import Link from '~/components/Link'
import TableList from '~/components/TableList'
import { cn } from '~/utils/cn'
import Code from '~/components/Code';
import Link from '~/components/Link';
import TableList from '~/components/TableList';
import { cn } from '~/utils/cn';
import AddDNS from '../dialogs/dns'
import AddDNS from '../dialogs/dns';
interface Props {
records: { name: string, type: 'A', value: string }[]
isDisabled: boolean
records: { name: string; type: 'A'; value: string }[];
isDisabled: boolean;
}
export default function DNS({ records, isDisabled }: Props) {
const submit = useSubmit()
const submit = useSubmit();
return (
<div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">DNS Records</h1>
<p className="text-gray-700 dark:text-gray-300">
Headscale supports adding custom DNS records to your Tailnet.
As of now, only
{' '}
<Code>A</Code>
{' '}
records are supported.
{' '}
Headscale supports adding custom DNS records to your Tailnet. As of now,
only <Code>A</Code> records are supported.{' '}
<Link
to="https://headscale.net/stable/ref/dns"
name="Headscale DNS Records documentation"
@ -36,15 +31,12 @@ export default function DNS({ records, isDisabled }: Props) {
</p>
<div className="mt-4">
<TableList className="mb-8">
{records.length === 0
? (
<TableList.Item>
<p className="opacity-50 text-sm mx-auto">
No DNS records found
</p>
</TableList.Item>
)
: records.map((record, index) => (
{records.length === 0 ? (
<TableList.Item>
<p className="opacity-50 text-sm mx-auto">No DNS records found</p>
</TableList.Item>
) : (
records.map((record, index) => (
<TableList.Item key={index}>
<div className="flex gap-24">
<div className="flex gap-2">
@ -62,27 +54,28 @@ export default function DNS({ records, isDisabled }: Props) {
)}
isDisabled={isDisabled}
onPress={() => {
submit({
'dns.extra_records': records
.filter((_, i) => i !== index),
}, {
method: 'PATCH',
encType: 'application/json',
})
submit(
{
'dns.extra_records': records.filter(
(_, i) => i !== index,
),
},
{
method: 'PATCH',
encType: 'application/json',
},
);
}}
>
Remove
</Button>
</TableList.Item>
))}
))
)}
</TableList>
{isDisabled
? undefined
: (
<AddDNS records={records} />
)}
{isDisabled ? undefined : <AddDNS records={records} />}
</div>
</div>
)
);
}

View file

@ -1,88 +1,88 @@
/* eslint-disable unicorn/no-keyword-prefix */
import {
closestCorners,
DndContext,
DragOverlay
} from '@dnd-kit/core'
import { closestCorners, DndContext, DragOverlay } from '@dnd-kit/core';
import {
restrictToParentElement,
restrictToVerticalAxis
} from '@dnd-kit/modifiers'
restrictToVerticalAxis,
} from '@dnd-kit/modifiers';
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { LockIcon, ThreeBarsIcon } from '@primer/octicons-react'
import { FetcherWithComponents, useFetcher } from '@remix-run/react'
import { useEffect, useState } from 'react'
import { Button, Input } from 'react-aria-components'
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { LockIcon, ThreeBarsIcon } from '@primer/octicons-react';
import { FetcherWithComponents, useFetcher } from 'react-router';
import { useEffect, useState } from 'react';
import { Button, Input } from 'react-aria-components';
import Spinner from '~/components/Spinner'
import TableList from '~/components/TableList'
import { cn } from '~/utils/cn'
import Spinner from '~/components/Spinner';
import TableList from '~/components/TableList';
import { cn } from '~/utils/cn';
type Properties = {
readonly baseDomain?: string;
readonly searchDomains: string[];
// eslint-disable-next-line react/boolean-prop-naming
readonly disabled?: boolean;
}
};
export default function Domains({ baseDomain, searchDomains, disabled }: Properties) {
export default function Domains({
baseDomain,
searchDomains,
disabled,
}: Properties) {
// eslint-disable-next-line unicorn/no-null, @typescript-eslint/ban-types
const [activeId, setActiveId] = useState<number | string | null>(null)
const [localDomains, setLocalDomains] = useState(searchDomains)
const [newDomain, setNewDomain] = useState('')
const fetcher = useFetcher()
const [activeId, setActiveId] = useState<number | string | null>(null);
const [localDomains, setLocalDomains] = useState(searchDomains);
const [newDomain, setNewDomain] = useState('');
const fetcher = useFetcher();
useEffect(() => {
setLocalDomains(searchDomains)
}, [searchDomains])
setLocalDomains(searchDomains);
}, [searchDomains]);
return (
<div className='flex flex-col w-2/3'>
<h1 className='text-2xl font-medium mb-4'>Search Domains</h1>
<p className='text-gray-700 dark:text-gray-300 mb-2'>
Set custom DNS search domains for your Tailnet.
When using Magic DNS, your tailnet domain is used as the first search domain.
<div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Search Domains</h1>
<p className="text-gray-700 dark:text-gray-300 mb-2">
Set custom DNS search domains for your Tailnet. When using Magic DNS,
your tailnet domain is used as the first search domain.
</p>
<DndContext
modifiers={[restrictToVerticalAxis, restrictToParentElement]}
collisionDetection={closestCorners}
onDragStart={event => {
setActiveId(event.active.id)
onDragStart={(event) => {
setActiveId(event.active.id);
}}
onDragEnd={event => {
onDragEnd={(event) => {
// eslint-disable-next-line unicorn/no-null
setActiveId(null)
const { active, over } = event
setActiveId(null);
const { active, over } = event;
if (!over) {
return
return;
}
const activeItem = localDomains[active.id as number - 1]
const overItem = localDomains[over.id as number - 1]
const activeItem = localDomains[(active.id as number) - 1];
const overItem = localDomains[(over.id as number) - 1];
if (!activeItem || !overItem) {
return
return;
}
const oldIndex = localDomains.indexOf(activeItem)
const newIndex = localDomains.indexOf(overItem)
const oldIndex = localDomains.indexOf(activeItem);
const newIndex = localDomains.indexOf(overItem);
if (oldIndex !== newIndex) {
setLocalDomains(arrayMove(localDomains, oldIndex, newIndex))
setLocalDomains(arrayMove(localDomains, oldIndex, newIndex));
}
}}
>
<TableList>
{baseDomain ? (
<TableList.Item key='magic-dns-sd'>
<p className='font-mono text-sm'>{baseDomain}</p>
<LockIcon className='h-4 w-4'/>
<TableList.Item key="magic-dns-sd">
<p className="font-mono text-sm">{baseDomain}</p>
<LockIcon className="h-4 w-4" />
</TableList.Item>
) : undefined}
<SortableContext
@ -101,25 +101,27 @@ export default function Domains({ baseDomain, searchDomains, disabled }: Propert
/>
))}
<DragOverlay adjustScale>
{activeId ? <Domain
isDrag
domain={localDomains[activeId as number - 1]}
localDomains={localDomains}
id={activeId as number - 1}
disabled={disabled}
fetcher={fetcher}
/> : undefined}
{activeId ? (
<Domain
isDrag
domain={localDomains[(activeId as number) - 1]}
localDomains={localDomains}
id={(activeId as number) - 1}
disabled={disabled}
fetcher={fetcher}
/>
) : undefined}
</DragOverlay>
</SortableContext>
{disabled ? undefined : (
<TableList.Item key='add-sd'>
<TableList.Item key="add-sd">
<Input
type='text'
className='font-mono text-sm bg-transparent w-full mr-2'
placeholder='Search Domain'
type="text"
className="font-mono text-sm bg-transparent w-full mr-2"
placeholder="Search Domain"
value={newDomain}
onChange={event => {
setNewDomain(event.target.value)
onChange={(event) => {
setNewDomain(event.target.value);
}}
/>
{fetcher.state === 'idle' ? (
@ -128,32 +130,35 @@ export default function Domains({ baseDomain, searchDomains, disabled }: Propert
'text-sm font-semibold',
'text-blue-600 dark:text-blue-400',
'hover:text-blue-700 dark:hover:text-blue-300',
newDomain.length === 0 && 'opacity-50 cursor-not-allowed'
newDomain.length === 0 && 'opacity-50 cursor-not-allowed',
)}
isDisabled={newDomain.length === 0}
onPress={() => {
fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention
'dns.search_domains': [...localDomains, newDomain]
}, {
method: 'PATCH',
encType: 'application/json'
})
fetcher.submit(
{
// eslint-disable-next-line @typescript-eslint/naming-convention
'dns.search_domains': [...localDomains, newDomain],
},
{
method: 'PATCH',
encType: 'application/json',
},
);
setNewDomain('')
setNewDomain('');
}}
>
Add
</Button>
) : (
<Spinner className='w-3 h-3 mr-0'/>
<Spinner className="w-3 h-3 mr-0" />
)}
</TableList.Item>
)}
</TableList>
</DndContext>
</div>
)
);
}
type DomainProperties = {
@ -164,17 +169,24 @@ type DomainProperties = {
// eslint-disable-next-line react/boolean-prop-naming
readonly disabled?: boolean;
readonly fetcher: FetcherWithComponents<unknown>;
}
};
function Domain({ domain, id, localDomains, isDrag, disabled, fetcher }: DomainProperties) {
function Domain({
domain,
id,
localDomains,
isDrag,
disabled,
fetcher,
}: DomainProperties) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging
} = useSortable({ id })
isDragging,
} = useSortable({ id });
// TODO: Figure out why TableList.Item breaks dndkit
return (
@ -184,17 +196,19 @@ function Domain({ domain, id, localDomains, isDrag, disabled, fetcher }: DomainP
'flex items-center justify-between px-3 py-2',
'border-b border-gray-200 last:border-b-0 dark:border-zinc-800',
isDragging ? 'text-gray-400' : '',
isDrag ? 'outline outline-1 outline-gray-500 bg-gray-200 dark:bg-zinc-800' : ''
isDrag
? 'outline outline-1 outline-gray-500 bg-gray-200 dark:bg-zinc-800'
: '',
)}
style={{
transform: CSS.Transform.toString(transform),
transition
transition,
}}
>
<p className='font-mono text-sm flex items-center gap-4'>
<p className="font-mono text-sm flex items-center gap-4">
{disabled ? undefined : (
<ThreeBarsIcon
className='h-4 w-4 text-gray-400 focus:outline-none'
className="h-4 w-4 text-gray-400 focus:outline-none"
{...attributes}
{...listeners}
/>
@ -207,21 +221,26 @@ function Domain({ domain, id, localDomains, isDrag, disabled, fetcher }: DomainP
'text-sm',
'text-red-600 dark:text-red-400',
'hover:text-red-700 dark:hover:text-red-300',
disabled && 'opacity-50 cursor-not-allowed'
disabled && 'opacity-50 cursor-not-allowed',
)}
isDisabled={disabled}
onPress={() => {
fetcher.submit({
'dns.search_domains': localDomains.filter((_, index) => index !== id - 1)
}, {
method: 'PATCH',
encType: 'application/json'
})
fetcher.submit(
{
'dns.search_domains': localDomains.filter(
(_, index) => index !== id - 1,
),
},
{
method: 'PATCH',
encType: 'application/json',
},
);
}}
>
Remove
</Button>
)}
</div>
)
);
}

View file

@ -1,53 +1,51 @@
import { useFetcher } from '@remix-run/react'
import { useFetcher } from 'react-router';
import Dialog from '~/components/Dialog'
import Spinner from '~/components/Spinner'
import Dialog from '~/components/Dialog';
import Spinner from '~/components/Spinner';
type Properties = {
readonly isEnabled: boolean;
readonly disabled?: boolean;
}
};
export default function Modal({ isEnabled, disabled }: Properties) {
const fetcher = useFetcher()
const fetcher = useFetcher();
return (
<Dialog>
<Dialog.Button isDisabled={disabled}>
{fetcher.state === 'idle' ? undefined : (
<Spinner className='w-3 h-3'/>
)}
{fetcher.state === 'idle' ? undefined : <Spinner className="w-3 h-3" />}
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
</Dialog.Button>
<Dialog.Panel>
{close => (
{(close) => (
<>
<Dialog.Title>
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
</Dialog.Title>
<Dialog.Text>
Devices will no longer be accessible via your tailnet domain.
The search domain will also be disabled.
Devices will no longer be accessible via your tailnet domain. The
search domain will also be disabled.
</Dialog.Text>
<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'
variant="confirm"
onPress={() => {
fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention
'dns.magic_dns': !isEnabled
}, {
method: 'PATCH',
encType: 'application/json'
})
fetcher.submit(
{
// eslint-disable-next-line @typescript-eslint/naming-convention
'dns.magic_dns': !isEnabled,
},
{
method: 'PATCH',
encType: 'application/json',
},
);
close()
close();
}}
>
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
@ -57,5 +55,5 @@ export default function Modal({ isEnabled, disabled }: Properties) {
)}
</Dialog.Panel>
</Dialog>
)
);
}

View file

@ -1,17 +1,17 @@
import { useSubmit } from '@remix-run/react'
import { useState } from 'react'
import { Button } from 'react-aria-components'
import { useSubmit } from 'react-router';
import { useState } from 'react';
import { Button } from 'react-aria-components';
import Link from '~/components/Link'
import Switch from '~/components/Switch'
import TableList from '~/components/TableList'
import { cn } from '~/utils/cn'
import Link from '~/components/Link';
import Switch from '~/components/Switch';
import TableList from '~/components/TableList';
import { cn } from '~/utils/cn';
import AddNameserver from '../dialogs/nameserver'
import AddNameserver from '../dialogs/nameserver';
interface Props {
nameservers: Record<string, string[]>
isDisabled: boolean
nameservers: Record<string, string[]>;
isDisabled: boolean;
}
export default function Nameservers({ nameservers, isDisabled }: Props) {
@ -19,9 +19,8 @@ export default function Nameservers({ nameservers, isDisabled }: Props) {
<div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Nameservers</h1>
<p className="text-gray-700 dark:text-gray-300">
Set the nameservers used by devices on the Tailnet
to resolve DNS queries.
{' '}
Set the nameservers used by devices on the Tailnet to resolve DNS
queries.{' '}
<Link
to="https://tailscale.com/kb/1054/dns"
name="Tailscale DNS Documentation"
@ -30,7 +29,7 @@ export default function Nameservers({ nameservers, isDisabled }: Props) {
</Link>
</p>
<div className="mt-4">
{Object.keys(nameservers).map(key => (
{Object.keys(nameservers).map((key) => (
<NameserverList
key={key}
isGlobal={key === 'global'}
@ -40,28 +39,29 @@ export default function Nameservers({ nameservers, isDisabled }: Props) {
/>
))}
{isDisabled
? undefined
: (
<AddNameserver nameservers={nameservers} />
)}
{isDisabled ? undefined : <AddNameserver nameservers={nameservers} />}
</div>
</div>
)
);
}
interface ListProps {
isGlobal: boolean
isDisabled: boolean
nameservers: string[]
name: string
isGlobal: boolean;
isDisabled: boolean;
nameservers: string[];
name: string;
}
function NameserverList({ isGlobal, isDisabled, nameservers, name }: ListProps) {
const submit = useSubmit()
const list = isGlobal ? nameservers['global'] : nameservers[name]
function NameserverList({
isGlobal,
isDisabled,
nameservers,
name,
}: ListProps) {
const submit = useSubmit();
const list = isGlobal ? nameservers['global'] : nameservers[name];
if (list.length === 0) {
return null
return null;
}
return (
@ -72,45 +72,54 @@ function NameserverList({ isGlobal, isDisabled, nameservers, name }: ListProps)
</h2>
</div>
<TableList>
{list.length > 0 ? list.map((ns, index) => (
// eslint-disable-next-line react/no-array-index-key
<TableList.Item key={index}>
<p className="font-mono text-sm">{ns}</p>
<Button
className={cn(
'text-sm',
'text-red-600 dark:text-red-400',
'hover:text-red-700 dark:hover:text-red-300',
isDisabled && 'opacity-50 cursor-not-allowed',
)}
isDisabled={isDisabled}
onPress={() => {
if (isGlobal) {
submit({
'dns.nameservers.global': list
.filter((_, i) => i !== index),
}, {
method: 'PATCH',
encType: 'application/json',
})
} else {
submit({
'dns.nameservers.split': {
...nameservers,
[name]: list.filter((_, i) => i !== index),
{list.length > 0
? list.map((ns, index) => (
// eslint-disable-next-line react/no-array-index-key
<TableList.Item key={index}>
<p className="font-mono text-sm">{ns}</p>
<Button
className={cn(
'text-sm',
'text-red-600 dark:text-red-400',
'hover:text-red-700 dark:hover:text-red-300',
isDisabled && 'opacity-50 cursor-not-allowed',
)}
isDisabled={isDisabled}
onPress={() => {
if (isGlobal) {
submit(
{
'dns.nameservers.global': list.filter(
(_, i) => i !== index,
),
},
{
method: 'PATCH',
encType: 'application/json',
},
);
} else {
submit(
{
'dns.nameservers.split': {
...nameservers,
[name]: list.filter((_, i) => i !== index),
},
},
{
method: 'PATCH',
encType: 'application/json',
},
);
}
}, {
method: 'PATCH',
encType: 'application/json',
})
}
}}
>
Remove
</Button>
</TableList.Item>
)) : undefined}
}}
>
Remove
</Button>
</TableList.Item>
))
: undefined}
</TableList>
</div>
)
);
}

View file

@ -1,34 +1,28 @@
import { useFetcher } from '@remix-run/react'
import { useState } from 'react'
import { Input } from 'react-aria-components'
import { useFetcher } from 'react-router';
import { useState } from 'react';
import { Input } from 'react-aria-components';
import Code from '~/components/Code'
import Dialog from '~/components/Dialog'
import Spinner from '~/components/Spinner'
import TextField from '~/components/TextField'
import { cn } from '~/utils/cn'
import Code from '~/components/Code';
import Dialog from '~/components/Dialog';
import Spinner from '~/components/Spinner';
import TextField from '~/components/TextField';
import { cn } from '~/utils/cn';
type Properties = {
readonly name: string;
readonly disabled?: boolean;
}
};
export default function Modal({ name, disabled }: Properties) {
const [newName, setNewName] = useState(name)
const fetcher = useFetcher()
const [newName, setNewName] = useState(name);
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'>
This is the base domain name of your Tailnet.
Devices are accessible at
{' '}
<Code>
[device].{name}
</Code>
{' '}
when Magic DNS is enabled.
<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">
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
@ -36,54 +30,54 @@ export default function Modal({ name, disabled }: Properties) {
'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'
'outline-none',
)}
type='text'
type="text"
value={name}
onFocus={event => {
event.target.select()
onFocus={(event) => {
event.target.select();
}}
/>
<Dialog>
<Dialog.Button isDisabled={disabled}>
{fetcher.state === 'idle' ? undefined : (
<Spinner className='w-3 h-3'/>
<Spinner className="w-3 h-3" />
)}
Rename Tailnet
</Dialog.Button>
<Dialog.Panel>
{close => (
{(close) => (
<>
<Dialog.Title>
Rename Tailnet
</Dialog.Title>
<Dialog.Title>Rename Tailnet</Dialog.Title>
<Dialog.Text>
Keep in mind that changing this can lead to all sorts of unexpected behavior and may break existing devices in your tailnet.
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
label='Tailnet name'
placeholder='ts.net'
label="Tailnet name"
placeholder="ts.net"
state={[newName, setNewName]}
className='my-2'
className="my-2"
/>
<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'
variant="confirm"
onPress={() => {
fetcher.submit({
'dns.base_domain': newName
}, {
method: 'PATCH',
encType: 'application/json'
})
fetcher.submit(
{
'dns.base_domain': newName,
},
{
method: 'PATCH',
encType: 'application/json',
},
);
close()
close();
}}
>
Rename
@ -94,5 +88,5 @@ export default function Modal({ name, disabled }: Properties) {
</Dialog.Panel>
</Dialog>
</div>
)
);
}