mirror of
https://github.com/tale/headplane.git
synced 2026-08-30 16:59:56 +00:00
feat: switch to normal form actions for dns
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
+67
-99
@@ -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>
|
||||
);
|
||||
+24
-43
@@ -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>
|
||||
);
|
||||
+12
-35
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user