feat: switch to normal form actions for dns

This commit is contained in:
Aarnav Tale
2025-02-13 12:29:16 -05:00
parent 2a1c795d46
commit 5be3cb345e
10 changed files with 448 additions and 391 deletions
-44
View File
@@ -1,44 +0,0 @@
import { useFetcher } from 'react-router';
import Dialog from '~/components/Dialog';
import Spinner from '~/components/Spinner';
type Properties = {
readonly isEnabled: boolean;
readonly disabled?: boolean;
};
// TODO: Use form action instead of JSON patching
// AND FIX JSON END OF UNEXPECTED INPUT
export default function Modal({ isEnabled, disabled }: Properties) {
const fetcher = useFetcher();
return (
<Dialog>
<Dialog.Button isDisabled={disabled}>
{fetcher.state === 'idle' ? undefined : <Spinner className="w-3 h-3" />}
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
</Dialog.Button>
<Dialog.Panel
onSubmit={() => {
fetcher.submit(
{
'dns.magic_dns': !isEnabled,
},
{
method: 'PATCH',
encType: 'application/json',
},
);
}}
>
<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.
</Dialog.Text>
</Dialog.Panel>
</Dialog>
);
}
@@ -1,4 +1,3 @@
/* eslint-disable unicorn/no-keyword-prefix */
import { DndContext, DragOverlay, closestCorners } from '@dnd-kit/core';
import {
restrictToParentElement,
@@ -13,29 +12,25 @@ import {
import { CSS } from '@dnd-kit/utilities';
import { GripVertical, Lock } from 'lucide-react';
import { useEffect, useState } from 'react';
import { type FetcherWithComponents, useFetcher } from 'react-router';
import { type FetcherWithComponents, Form, useFetcher } from 'react-router';
import Button from '~/components/Button';
import Input from '~/components/Input';
import Spinner from '~/components/Spinner';
import TableList from '~/components/TableList';
import cn from '~/utils/cn';
type Properties = {
readonly baseDomain?: string;
readonly searchDomains: string[];
readonly disabled?: boolean; // TODO: isDisabled
};
interface Props {
searchDomains: string[];
isDisabled: boolean;
magic?: string;
}
export default function Domains({
baseDomain,
export default function ManageDomains({
searchDomains,
disabled,
}: Properties) {
isDisabled,
magic,
}: Props) {
const [activeId, setActiveId] = useState<number | string | null>(null);
const [localDomains, setLocalDomains] = useState(searchDomains);
const [newDomain, setNewDomain] = useState('');
const fetcher = useFetcher();
useEffect(() => {
setLocalDomains(searchDomains);
@@ -77,16 +72,16 @@ export default function Domains({
}}
>
<TableList>
{baseDomain ? (
{magic ? (
<TableList.Item key="magic-dns-sd">
<div
className={cn(
'flex items-center gap-4',
disabled ? 'flex-row-reverse justify-between w-full' : '',
isDisabled ? 'flex-row-reverse justify-between w-full' : '',
)}
>
<Lock className="p-0.5" />
<p className="font-mono text-sm py-0.5">{baseDomain}</p>
<p className="font-mono text-sm py-0.5">{magic}</p>
</div>
</TableList.Item>
) : undefined}
@@ -99,63 +94,49 @@ export default function Domains({
key={sd}
domain={sd}
id={index + 1}
localDomains={localDomains}
disabled={disabled}
fetcher={fetcher}
isDisabled={isDisabled}
/>
))}
<DragOverlay adjustScale>
{activeId ? (
<Domain
isDrag
isDragging
domain={localDomains[(activeId as number) - 1]}
localDomains={localDomains}
id={(activeId as number) - 1}
disabled={disabled}
fetcher={fetcher}
isDisabled={isDisabled}
/>
) : undefined}
</DragOverlay>
</SortableContext>
{disabled ? undefined : (
{isDisabled ? undefined : (
<TableList.Item key="add-sd">
<Input
type="text"
className={cn(
'border-none font-mono p-0',
'rounded-none focus:ring-0 w-full',
)}
placeholder="Search Domain"
onChange={setNewDomain}
label="Search Domain"
labelHidden
/>
{fetcher.state === 'idle' ? (
<Form
method="POST"
className="flex items-center justify-between w-full"
>
<input type="hidden" name="action_id" value="add_domain" />
<Input
type="text"
className={cn(
'border-none font-mono p-0 text-sm',
'rounded-none focus:ring-0 w-full ml-1',
)}
placeholder="Search Domain"
label="Search Domain"
name="domain"
labelHidden
isRequired
/>
<Button
type="submit"
className={cn(
'px-2 py-1 rounded-md',
'text-blue-500 dark:text-blue-400',
)}
isDisabled={newDomain.length === 0}
onPress={() => {
fetcher.submit(
{
'dns.search_domains': [...localDomains, newDomain],
},
{
method: 'PATCH',
encType: 'application/json',
},
);
setNewDomain('');
}}
>
Add
</Button>
) : (
<Spinner className="w-3 h-3 mr-0" />
)}
</Form>
</TableList.Item>
)}
</TableList>
@@ -164,38 +145,29 @@ export default function Domains({
);
}
type DomainProperties = {
readonly domain: string;
readonly id: number;
readonly isDrag?: boolean;
readonly localDomains: string[];
readonly disabled?: boolean; // TODO: isDisabled
readonly fetcher: FetcherWithComponents<unknown>;
};
interface DomainProps {
domain: string;
id: number;
isDragging?: boolean;
isDisabled: boolean;
}
function Domain({
domain,
id,
localDomains,
isDrag,
disabled,
fetcher,
}: DomainProperties) {
function Domain({ domain, id, isDragging, isDisabled }: DomainProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
isDragging: isSortableDragging,
} = useSortable({ id });
return (
<TableList.Item
ref={setNodeRef}
className={cn(
isDragging ? 'opacity-50' : '',
isDrag ? 'ring bg-white dark:bg-headplane-900' : '',
isSortableDragging ? 'opacity-50' : '',
isDragging ? 'ring bg-white dark:bg-headplane-900' : '',
)}
style={{
transform: CSS.Transform.toString(transform),
@@ -203,34 +175,30 @@ function Domain({
}}
>
<p className="font-mono text-sm flex items-center gap-4">
{disabled ? undefined : (
<GripVertical {...attributes} {...listeners} className="p-0.5" />
{isDisabled ? undefined : (
<GripVertical
{...attributes}
{...listeners}
className="p-0.5 focus:ring outline-none rounded-md"
/>
)}
{domain}
</p>
{isDrag ? undefined : (
<Button
className={cn(
'px-2 py-1 rounded-md',
'text-red-500 dark:text-red-400',
)}
isDisabled={disabled}
onPress={() => {
fetcher.submit(
{
'dns.search_domains': localDomains.filter(
(_, index) => index !== id - 1,
),
},
{
method: 'PATCH',
encType: 'application/json',
},
);
}}
>
Remove
</Button>
{isDragging ? undefined : (
<Form method="POST">
<input type="hidden" name="action_id" value="remove_domain" />
<input type="hidden" name="domain" value={domain} />
<Button
type="submit"
isDisabled={isDisabled}
className={cn(
'px-2 py-1 rounded-md',
'text-red-500 dark:text-red-400',
)}
>
Remove
</Button>
</Form>
)}
</TableList.Item>
);
@@ -1,16 +1,16 @@
import { useSubmit } from 'react-router';
import { Form } from 'react-router';
import Button from '~/components/Button';
import Link from '~/components/Link';
import TableList from '~/components/TableList';
import cn from '~/utils/cn';
import AddNameserver from '../dialogs/nameserver';
import AddNS from '../dialogs/add-ns';
interface Props {
nameservers: Record<string, string[]>;
isDisabled: boolean;
}
export default function Nameservers({ nameservers, isDisabled }: Props) {
export default function ManageNS({ nameservers, isDisabled }: Props) {
return (
<div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Nameservers</h1>
@@ -35,7 +35,7 @@ export default function Nameservers({ nameservers, isDisabled }: Props) {
/>
))}
{isDisabled ? undefined : <AddNameserver nameservers={nameservers} />}
{isDisabled ? undefined : <AddNS nameservers={nameservers} />}
</div>
</div>
);
@@ -54,7 +54,6 @@ function NameserverList({
nameservers,
name,
}: ListProps) {
const submit = useSubmit();
const list = isGlobal ? nameservers.global : nameservers[name];
if (list.length === 0) {
return null;
@@ -69,46 +68,28 @@ function NameserverList({
</div>
<TableList>
{list.length > 0
? list.map((ns, index) => (
? list.map((ns) => (
<TableList.Item key={ns}>
<p className="font-mono text-sm">{ns}</p>
<Button
className={cn(
'px-2 py-1 rounded-md',
'text-red-500 dark:text-red-400',
)}
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',
},
);
}
}}
>
Remove
</Button>
<Form method="POST">
<input type="hidden" name="action_id" value="remove_ns" />
<input type="hidden" name="ns" value={ns} />
<input
type="hidden"
name="split_name"
value={isGlobal ? 'global' : name}
/>
<Button
isDisabled={isDisabled}
type="submit"
className={cn(
'px-2 py-1 rounded-md',
'text-red-500 dark:text-red-400',
)}
>
Remove
</Button>
</Form>
</TableList.Item>
))
: undefined}
@@ -1,19 +1,17 @@
import { useSubmit } from 'react-router';
import { Form } from 'react-router';
import Button from '~/components/Button';
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 AddRecord from '../dialogs/add-record';
interface Props {
records: { name: string; type: 'A'; value: string }[];
records: { name: string; type: 'A' | string; value: string }[];
isDisabled: boolean;
}
export default function DNS({ records, isDisabled }: Props) {
const submit = useSubmit();
export default function ManageRecords({ records, isDisabled }: Props) {
return (
<div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">DNS Records</h1>
@@ -50,34 +48,27 @@ export default function DNS({ records, isDisabled }: Props) {
</div>
<p className="font-mono text-sm">{record.value}</p>
</div>
<Button
className={cn(
'px-2 py-1 rounded-md',
'text-red-500 dark:text-red-400',
)}
isDisabled={isDisabled}
onPress={() => {
submit(
{
'dns.extra_records': records.filter(
(_, i) => i !== index,
),
},
{
method: 'PATCH',
encType: 'application/json',
},
);
}}
>
Remove
</Button>
<Form method="POST">
<input type="hidden" name="action_id" value="remove_record" />
<input type="hidden" name="record_name" value={record.name} />
<input type="hidden" name="record_type" value={record.type} />
<Button
type="submit"
isDisabled={isDisabled}
className={cn(
'px-2 py-1 rounded-md',
'text-red-500 dark:text-red-400',
)}
>
Remove
</Button>
</Form>
</TableList.Item>
))
)}
</TableList>
{isDisabled ? undefined : <AddDNS records={records} />}
{isDisabled ? undefined : <AddRecord records={records} />}
</div>
</div>
);
@@ -1,22 +1,13 @@
import { useState } from 'react';
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 cn from '~/utils/cn';
type Properties = {
readonly name: string;
readonly disabled?: boolean;
};
// TODO: Switch to form submit instead of JSON patch
export default function Modal({ name, disabled }: Properties) {
const [newName, setNewName] = useState(name);
const fetcher = useFetcher();
interface Props {
name: string;
isDisabled: boolean;
}
export default function RenameTailnet({ name, isDisabled }: Props) {
return (
<div className="flex flex-col w-2/3 gap-y-4">
<h1 className="text-2xl font-medium mb-2">Tailnet Name</h1>
@@ -35,34 +26,20 @@ export default function Modal({ name, disabled }: Properties) {
}}
/>
<Dialog>
<Dialog.Button isDisabled={disabled}>
{fetcher.state === 'idle' ? undefined : (
<Spinner className="w-3 h-3" />
)}
Rename Tailnet
</Dialog.Button>
<Dialog.Panel
onSubmit={() => {
fetcher.submit(
{
'dns.base_domain': newName,
},
{
method: 'PATCH',
encType: 'application/json',
},
);
}}
>
<Dialog.Button isDisabled={isDisabled}>Rename Tailnet</Dialog.Button>
<Dialog.Panel isDisabled={isDisabled}>
<Dialog.Title>Rename Tailnet</Dialog.Title>
<Dialog.Text>
<Dialog.Text className="mb-8">
Keep in mind that changing this can lead to all sorts of unexpected
behavior and may break existing devices in your tailnet.
</Dialog.Text>
<input type="hidden" name="action_id" value="rename_tailnet" />
<Input
isRequired
label="Tailnet name"
placeholder="ts.net"
onChange={setNewName}
defaultValue={name}
name="new_name"
/>
</Dialog.Panel>
</Dialog>
@@ -0,0 +1,31 @@
import Dialog from '~/components/Dialog';
interface Props {
isEnabled: boolean;
isDisabled: boolean;
}
export default function Modal({ isEnabled, isDisabled }: Props) {
return (
<Dialog>
<Dialog.Button isDisabled={isDisabled}>
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
</Dialog.Button>
<Dialog.Panel isDisabled={isDisabled}>
<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.
</Dialog.Text>
<input type="hidden" name="action_id" value="toggle_magic" />
<input
type="hidden"
name="new_state"
value={isEnabled ? 'disabled' : 'enabled'}
/>
</Dialog.Panel>
</Dialog>
);
}