mirror of
https://github.com/tale/headplane.git
synced 2026-08-30 00:47:17 +00:00
feat: swap button and decompose dialog components
This commit is contained in:
@@ -1,94 +1,93 @@
|
||||
import { Split } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import Chip from '~/components/Chip';
|
||||
import Dialog from '~/components/Dialog';
|
||||
import Input from '~/components/Input';
|
||||
import Switch from '~/components/Switch';
|
||||
import Tooltip from '~/components/Tooltip';
|
||||
import cn from '~/utils/cn';
|
||||
import { Split } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Chip from "~/components/Chip";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Input from "~/components/Input";
|
||||
import Switch from "~/components/Switch";
|
||||
import Text from "~/components/Text";
|
||||
import Title from "~/components/Title";
|
||||
import Tooltip from "~/components/Tooltip";
|
||||
import cn from "~/utils/cn";
|
||||
|
||||
interface Props {
|
||||
nameservers: Record<string, string[]>;
|
||||
nameservers: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export default function AddNameserver({ nameservers }: Props) {
|
||||
const [split, setSplit] = useState(false);
|
||||
const [ns, setNs] = useState('');
|
||||
const [domain, setDomain] = useState('');
|
||||
const [split, setSplit] = useState(false);
|
||||
const [ns, setNs] = useState("");
|
||||
const [domain, setDomain] = useState("");
|
||||
|
||||
const isInvalid = useMemo(() => {
|
||||
if (ns === '') return false;
|
||||
// Test if it's a valid IPv4 or IPv6 address
|
||||
const ipv4 = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||
const ipv6 = /^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$/;
|
||||
if (!ipv4.test(ns) && !ipv6.test(ns)) return true;
|
||||
const isInvalid = useMemo(() => {
|
||||
if (ns === "") return false;
|
||||
// Test if it's a valid IPv4 or IPv6 address
|
||||
const ipv4 = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||
const ipv6 = /^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$/;
|
||||
if (!ipv4.test(ns) && !ipv6.test(ns)) return true;
|
||||
|
||||
if (split) {
|
||||
return nameservers[domain]?.includes(ns);
|
||||
}
|
||||
if (split) {
|
||||
return nameservers[domain]?.includes(ns);
|
||||
}
|
||||
|
||||
return Object.values(nameservers).some((nsList) => nsList.includes(ns));
|
||||
}, [nameservers, ns]);
|
||||
return Object.values(nameservers).some((nsList) => nsList.includes(ns));
|
||||
}, [nameservers, ns]);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Button>Add nameserver</Dialog.Button>
|
||||
<Dialog.Panel>
|
||||
<Dialog.Title className="mb-4">Add nameserver</Dialog.Title>
|
||||
<input name="action_id" type="hidden" value="add_ns" />
|
||||
<Input
|
||||
description="Use this IPv4 or IPv6 address to resolve names."
|
||||
isInvalid={isInvalid}
|
||||
isRequired
|
||||
label="Nameserver"
|
||||
name="ns"
|
||||
onChange={setNs}
|
||||
placeholder="1.2.3.4"
|
||||
/>
|
||||
<div className="flex items-center justify-between mt-8">
|
||||
<div className="block">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Dialog.Text className="font-semibold">
|
||||
Restrict to domain
|
||||
</Dialog.Text>
|
||||
<Tooltip>
|
||||
<Chip
|
||||
className={cn('inline-flex items-center')}
|
||||
leftIcon={<Split className="w-3 h-3 mr-0.5" />}
|
||||
text="Split DNS"
|
||||
/>
|
||||
<Tooltip.Body>
|
||||
Only clients that support split DNS (Tailscale v1.8 or later
|
||||
for most platforms) will use this nameserver. Older clients
|
||||
will ignore it.
|
||||
</Tooltip.Body>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Dialog.Text className="text-sm">
|
||||
This nameserver will only be used for some domains.
|
||||
</Dialog.Text>
|
||||
</div>
|
||||
<Switch label="Split DNS" onChange={setSplit} />
|
||||
</div>
|
||||
{split ? (
|
||||
<>
|
||||
<Dialog.Text className="font-semibold mt-8">Domain</Dialog.Text>
|
||||
<Input
|
||||
isRequired={split === true}
|
||||
label="Domain"
|
||||
name="split_name"
|
||||
onChange={setDomain}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
<Dialog.Text className="text-sm">
|
||||
Only single-label or fully-qualified queries matching this suffix
|
||||
should use the nameserver.
|
||||
</Dialog.Text>
|
||||
</>
|
||||
) : (
|
||||
<input name="split_name" type="hidden" value="global" />
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
return (
|
||||
<Dialog>
|
||||
<Button>Add nameserver</Button>
|
||||
<DialogPanel>
|
||||
<Title className="mb-4">Add nameserver</Title>
|
||||
<input name="action_id" type="hidden" value="add_ns" />
|
||||
<Input
|
||||
description="Use this IPv4 or IPv6 address to resolve names."
|
||||
isInvalid={isInvalid}
|
||||
isRequired
|
||||
label="Nameserver"
|
||||
name="ns"
|
||||
onChange={setNs}
|
||||
placeholder="1.2.3.4"
|
||||
/>
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<div className="block">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Text className="font-semibold">Restrict to domain</Text>
|
||||
<Tooltip>
|
||||
<Chip
|
||||
className={cn("inline-flex items-center")}
|
||||
leftIcon={<Split className="mr-0.5 h-3 w-3" />}
|
||||
text="Split DNS"
|
||||
/>
|
||||
<Tooltip.Body>
|
||||
Only clients that support split DNS (Tailscale v1.8 or later for most platforms)
|
||||
will use this nameserver. Older clients will ignore it.
|
||||
</Tooltip.Body>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Text className="text-sm">This nameserver will only be used for some domains.</Text>
|
||||
</div>
|
||||
<Switch label="Split DNS" onChange={setSplit} />
|
||||
</div>
|
||||
{split ? (
|
||||
<>
|
||||
<Text className="mt-8 font-semibold">Domain</Text>
|
||||
<Input
|
||||
isRequired={split === true}
|
||||
label="Domain"
|
||||
name="split_name"
|
||||
onChange={setDomain}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
<Text className="text-sm">
|
||||
Only single-label or fully-qualified queries matching this suffix should use the
|
||||
nameserver.
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<input name="split_name" type="hidden" value="global" />
|
||||
)}
|
||||
</DialogPanel>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,79 +1,79 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import Code from '~/components/Code';
|
||||
import Dialog from '~/components/Dialog';
|
||||
import Input from '~/components/Input';
|
||||
import Select from '~/components/Select';
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Code from "~/components/Code";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Input from "~/components/Input";
|
||||
import Select from "~/components/Select";
|
||||
import Text from "~/components/Text";
|
||||
import Title from "~/components/Title";
|
||||
|
||||
interface Props {
|
||||
records: { name: string; type: 'A' | 'AAAA' | string; value: string }[];
|
||||
records: { name: string; type: "A" | "AAAA" | string; value: string }[];
|
||||
}
|
||||
|
||||
export default function AddRecord({ records }: Props) {
|
||||
const [type, setType] = useState<'A' | 'AAAA' | string>('A');
|
||||
const [name, setName] = useState('');
|
||||
const [ip, setIp] = useState('');
|
||||
const [type, setType] = useState<"A" | "AAAA" | string>("A");
|
||||
const [name, setName] = useState("");
|
||||
const [ip, setIp] = useState("");
|
||||
|
||||
const isDuplicate = useMemo(() => {
|
||||
if (name.length === 0 || ip.length === 0) return false;
|
||||
const lookup = records.find((record) => record.name === name);
|
||||
if (!lookup) return false;
|
||||
const isDuplicate = useMemo(() => {
|
||||
if (name.length === 0 || ip.length === 0) return false;
|
||||
const lookup = records.find((record) => record.name === name);
|
||||
if (!lookup) return false;
|
||||
|
||||
return lookup.value === ip;
|
||||
}, [records, name, ip]);
|
||||
return lookup.value === ip;
|
||||
}, [records, name, ip]);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Button>Add DNS record</Dialog.Button>
|
||||
<Dialog.Panel
|
||||
onSubmit={() => {
|
||||
setName('');
|
||||
setIp('');
|
||||
}}
|
||||
>
|
||||
<Dialog.Title>Add DNS record</Dialog.Title>
|
||||
<Dialog.Text>
|
||||
Enter the domain and IP address for the new DNS record.
|
||||
</Dialog.Text>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<input type="hidden" name="action_id" value="add_record" />
|
||||
<Select
|
||||
isRequired
|
||||
label="Record Type"
|
||||
name="record_type"
|
||||
defaultInputValue={type}
|
||||
onSelectionChange={(v) => {
|
||||
if (v) setType(v.toString() as 'A' | 'AAAA');
|
||||
}}
|
||||
>
|
||||
<Select.Item key="A">A</Select.Item>
|
||||
<Select.Item key="AAAA">AAAA</Select.Item>
|
||||
</Select>
|
||||
<Input
|
||||
isRequired
|
||||
label="Domain"
|
||||
placeholder="test.example.com"
|
||||
name="record_name"
|
||||
onChange={setName}
|
||||
isInvalid={isDuplicate}
|
||||
/>
|
||||
<Input
|
||||
isRequired
|
||||
label="IP Address"
|
||||
placeholder={
|
||||
type === 'AAAA' ? '2001:db8::ff00:42:8329' : '101.101.101.101'
|
||||
}
|
||||
name="record_value"
|
||||
onChange={setIp}
|
||||
isInvalid={isDuplicate}
|
||||
/>
|
||||
{isDuplicate ? (
|
||||
<p className="text-sm opacity-50">
|
||||
A record with the domain name <Code>{name}</Code> and IP address{' '}
|
||||
<Code>{ip}</Code> already exists.
|
||||
</p>
|
||||
) : undefined}
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
return (
|
||||
<Dialog>
|
||||
<Button>Add DNS record</Button>
|
||||
<DialogPanel
|
||||
onSubmit={() => {
|
||||
setName("");
|
||||
setIp("");
|
||||
}}
|
||||
>
|
||||
<Title>Add DNS record</Title>
|
||||
<Text>Enter the domain and IP address for the new DNS record.</Text>
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<input type="hidden" name="action_id" value="add_record" />
|
||||
<Select
|
||||
isRequired
|
||||
label="Record Type"
|
||||
name="record_type"
|
||||
defaultInputValue={type}
|
||||
onSelectionChange={(v) => {
|
||||
if (v) setType(v.toString() as "A" | "AAAA");
|
||||
}}
|
||||
>
|
||||
<Select.Item key="A">A</Select.Item>
|
||||
<Select.Item key="AAAA">AAAA</Select.Item>
|
||||
</Select>
|
||||
<Input
|
||||
isRequired
|
||||
label="Domain"
|
||||
placeholder="test.example.com"
|
||||
name="record_name"
|
||||
onChange={setName}
|
||||
isInvalid={isDuplicate}
|
||||
/>
|
||||
<Input
|
||||
isRequired
|
||||
label="IP Address"
|
||||
placeholder={type === "AAAA" ? "2001:db8::ff00:42:8329" : "101.101.101.101"}
|
||||
name="record_value"
|
||||
onChange={setIp}
|
||||
isInvalid={isDuplicate}
|
||||
/>
|
||||
{isDuplicate ? (
|
||||
<p className="text-sm opacity-50">
|
||||
A record with the domain name <Code>{name}</Code> and IP address <Code>{ip}</Code>{" "}
|
||||
already exists.
|
||||
</p>
|
||||
) : undefined}
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user