mirror of
https://github.com/tale/headplane.git
synced 2026-08-27 23:47:31 +00:00
feat: swap button and decompose dialog components
This commit is contained in:
@@ -2,14 +2,16 @@ import type { Key } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useFetcher } from "react-router";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Button from "~/components/button";
|
||||
import Code from "~/components/Code";
|
||||
import Dialog from "~/components/Dialog";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Input from "~/components/Input";
|
||||
import Link from "~/components/link";
|
||||
import NumberInput from "~/components/NumberInput";
|
||||
import Select from "~/components/Select";
|
||||
import Switch from "~/components/Switch";
|
||||
import Text from "~/components/Text";
|
||||
import Title from "~/components/Title";
|
||||
import type { User } from "~/types";
|
||||
import toast from "~/utils/toast";
|
||||
import { getUserDisplayName } from "~/utils/user";
|
||||
@@ -91,11 +93,9 @@ export default function AddAuthKey({
|
||||
Create pre-auth key
|
||||
</Button>
|
||||
{createdKey ? (
|
||||
<Dialog.Panel variant="unactionable">
|
||||
<Dialog.Title>Pre-auth key created</Dialog.Title>
|
||||
<Dialog.Text>
|
||||
Copy this key now. You will not be able to see the full key again.
|
||||
</Dialog.Text>
|
||||
<DialogPanel variant="unactionable">
|
||||
<Title>Pre-auth key created</Title>
|
||||
<Text>Copy this key now. You will not be able to see the full key again.</Text>
|
||||
<div className="mt-4 flex items-center gap-2 rounded-lg bg-mist-100 px-3 py-2 dark:bg-mist-800">
|
||||
<code className="min-w-0 flex-1 truncate font-mono text-sm">{createdKey}</code>
|
||||
<Button
|
||||
@@ -109,13 +109,13 @@ export default function AddAuthKey({
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
<Dialog.Text className="mt-4 text-sm">To register a device with this key:</Dialog.Text>
|
||||
<Text className="mt-4 text-sm">To register a device with this key:</Text>
|
||||
<Code isCopyable className="mt-1 block text-sm">
|
||||
{`tailscale up --login-server=${url} --authkey ${createdKey}`}
|
||||
</Code>
|
||||
</Dialog.Panel>
|
||||
</DialogPanel>
|
||||
) : (
|
||||
<Dialog.Panel
|
||||
<DialogPanel
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
submittingRef.current = true;
|
||||
@@ -129,15 +129,13 @@ export default function AddAuthKey({
|
||||
}}
|
||||
isDisabled={fetcher.state !== "idle" || !canSubmit}
|
||||
>
|
||||
<Dialog.Title>Generate auth key</Dialog.Title>
|
||||
<Title>Generate auth key</Title>
|
||||
|
||||
{!selfServiceOnly && (
|
||||
<div className="mb-4 flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<Dialog.Text className="font-semibold">Tag-only key</Dialog.Text>
|
||||
<Dialog.Text className="text-sm">
|
||||
Create a key owned by ACL tags instead of a user.
|
||||
</Dialog.Text>
|
||||
<Text className="font-semibold">Tag-only key</Text>
|
||||
<Text className="text-sm">Create a key owned by ACL tags instead of a user.</Text>
|
||||
</div>
|
||||
<Switch
|
||||
defaultSelected={tagOnly}
|
||||
@@ -193,10 +191,8 @@ export default function AddAuthKey({
|
||||
/>
|
||||
<div className="mt-6 flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<Dialog.Text className="font-semibold">Reusable</Dialog.Text>
|
||||
<Dialog.Text className="text-sm">
|
||||
Use this key to authenticate more than one device.
|
||||
</Dialog.Text>
|
||||
<Text className="font-semibold">Reusable</Text>
|
||||
<Text className="text-sm">Use this key to authenticate more than one device.</Text>
|
||||
</div>
|
||||
<Switch
|
||||
defaultSelected={reusable}
|
||||
@@ -206,14 +202,14 @@ export default function AddAuthKey({
|
||||
</div>
|
||||
<div className="mt-6 flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<Dialog.Text className="font-semibold">Ephemeral</Dialog.Text>
|
||||
<Dialog.Text className="text-sm">
|
||||
<Text className="font-semibold">Ephemeral</Text>
|
||||
<Text className="text-sm">
|
||||
Devices authenticated with this key will be automatically removed once they go
|
||||
offline.{" "}
|
||||
<Link external styled to="https://tailscale.com/kb/1111/ephemeral-nodes">
|
||||
Learn more
|
||||
</Link>
|
||||
</Dialog.Text>
|
||||
</Text>
|
||||
</div>
|
||||
<Switch
|
||||
defaultSelected={ephemeral}
|
||||
@@ -221,7 +217,7 @@ export default function AddAuthKey({
|
||||
onChange={() => setEphemeral(!ephemeral)}
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</DialogPanel>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
import Dialog from '~/components/Dialog';
|
||||
import type { PreAuthKey, User } from '~/types';
|
||||
import Button from "~/components/Button";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Text from "~/components/Text";
|
||||
import Title from "~/components/Title";
|
||||
import type { PreAuthKey, User } from "~/types";
|
||||
|
||||
interface ExpireAuthKeyProps {
|
||||
authKey: PreAuthKey;
|
||||
user: User;
|
||||
authKey: PreAuthKey;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export default function ExpireAuthKey({ authKey, user }: ExpireAuthKeyProps) {
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Button variant="heavy">Expire Key</Dialog.Button>
|
||||
<Dialog.Panel variant="destructive">
|
||||
<Dialog.Title>Expire auth key?</Dialog.Title>
|
||||
<input name="action_id" type="hidden" value="expire_preauthkey" />
|
||||
<input name="user_id" type="hidden" value={user.id} />
|
||||
<input name="key" type="hidden" value={authKey.key} />
|
||||
<Dialog.Text>
|
||||
Expiring this authentication key will immediately prevent it from
|
||||
being used to authenticate new devices. This action cannot be undone.
|
||||
</Dialog.Text>
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
return (
|
||||
<Dialog>
|
||||
<Button variant="heavy">Expire Key</Button>
|
||||
<DialogPanel variant="destructive">
|
||||
<Title>Expire auth key?</Title>
|
||||
<input name="action_id" type="hidden" value="expire_preauthkey" />
|
||||
<input name="user_id" type="hidden" value={user.id} />
|
||||
<input name="key" type="hidden" value={authKey.key} />
|
||||
<Text>
|
||||
Expiring this authentication key will immediately prevent it from being used to
|
||||
authenticate new devices. This action cannot be undone.
|
||||
</Text>
|
||||
</DialogPanel>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,64 +1,68 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import Dialog from '~/components/Dialog';
|
||||
import Input from '~/components/Input';
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Input from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
import Title from "~/components/Title";
|
||||
|
||||
interface AddDomainProps {
|
||||
domains: string[];
|
||||
isDisabled?: boolean;
|
||||
domains: string[];
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export default function AddDomain({ domains, isDisabled }: AddDomainProps) {
|
||||
const [domain, setDomain] = useState('');
|
||||
const [domain, setDomain] = useState("");
|
||||
|
||||
const isInvalid = useMemo(() => {
|
||||
if (!domain || domain.trim().length === 0) {
|
||||
// Empty domain is invalid, but no error shown
|
||||
return false;
|
||||
}
|
||||
const isInvalid = useMemo(() => {
|
||||
if (!domain || domain.trim().length === 0) {
|
||||
// Empty domain is invalid, but no error shown
|
||||
return false;
|
||||
}
|
||||
|
||||
if (domains.includes(domain.trim())) {
|
||||
return true;
|
||||
}
|
||||
if (domains.includes(domain.trim())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if domain is a valid FQDN
|
||||
const url = new URL(`http://${domain.trim()}`);
|
||||
return url.hostname !== domain.trim();
|
||||
} catch (e) {
|
||||
// If URL constructor fails, it's not a valid domain
|
||||
return true;
|
||||
}
|
||||
}, [domain, domains]);
|
||||
try {
|
||||
// Check if domain is a valid FQDN
|
||||
const url = new URL(`http://${domain.trim()}`);
|
||||
return url.hostname !== domain.trim();
|
||||
} catch (e) {
|
||||
// If URL constructor fails, it's not a valid domain
|
||||
return true;
|
||||
}
|
||||
}, [domain, domains]);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Button isDisabled={isDisabled}>Add domain</Dialog.Button>
|
||||
<Dialog.Panel>
|
||||
<Dialog.Title>Add domain</Dialog.Title>
|
||||
<Dialog.Text className="mb-4">
|
||||
Add this domain to a list of allowed email domains that can
|
||||
authenticate with Headscale via OIDC.
|
||||
</Dialog.Text>
|
||||
<input name="action_id" type="hidden" value="add_domain" />
|
||||
<Input
|
||||
description={
|
||||
domain.trim().length > 0
|
||||
? `Matches users with <user>@${domain.trim()}`
|
||||
: 'Enter a domain to match users with their email addresses.'
|
||||
}
|
||||
isInvalid={domain.trim().length === 0 || isInvalid}
|
||||
isRequired
|
||||
label="Domain"
|
||||
name="domain"
|
||||
onChange={setDomain}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
{isInvalid && (
|
||||
<p className="text-red-500 text-sm mt-2">
|
||||
The domain you entered is invalid or already exists in the list.
|
||||
</p>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
return (
|
||||
<Dialog>
|
||||
<Button isDisabled={isDisabled}>Add domain</Button>
|
||||
<DialogPanel>
|
||||
<Title>Add domain</Title>
|
||||
<Text className="mb-4">
|
||||
Add this domain to a list of allowed email domains that can authenticate with Headscale
|
||||
via OIDC.
|
||||
</Text>
|
||||
<input name="action_id" type="hidden" value="add_domain" />
|
||||
<Input
|
||||
description={
|
||||
domain.trim().length > 0
|
||||
? `Matches users with <user>@${domain.trim()}`
|
||||
: "Enter a domain to match users with their email addresses."
|
||||
}
|
||||
isInvalid={domain.trim().length === 0 || isInvalid}
|
||||
isRequired
|
||||
label="Domain"
|
||||
name="domain"
|
||||
onChange={setDomain}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
{isInvalid && (
|
||||
<p className="mt-2 text-sm text-red-500">
|
||||
The domain you entered is invalid or already exists in the list.
|
||||
</p>
|
||||
)}
|
||||
</DialogPanel>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,51 +1,54 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import Dialog from '~/components/Dialog';
|
||||
import Input from '~/components/Input';
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Input from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
import Title from "~/components/Title";
|
||||
|
||||
interface AddGroupProps {
|
||||
groups: string[];
|
||||
isDisabled?: boolean;
|
||||
groups: string[];
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export default function AddGroup({ groups, isDisabled }: AddGroupProps) {
|
||||
const [group, setGroup] = useState('');
|
||||
const [group, setGroup] = useState("");
|
||||
|
||||
const isInvalid = useMemo(() => {
|
||||
if (!group || group.trim().length === 0) {
|
||||
// Empty group is invalid, but no error shown
|
||||
return false;
|
||||
}
|
||||
const isInvalid = useMemo(() => {
|
||||
if (!group || group.trim().length === 0) {
|
||||
// Empty group is invalid, but no error shown
|
||||
return false;
|
||||
}
|
||||
|
||||
if (groups.includes(group.trim())) {
|
||||
return true;
|
||||
}
|
||||
}, [group, groups]);
|
||||
if (groups.includes(group.trim())) {
|
||||
return true;
|
||||
}
|
||||
}, [group, groups]);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Button isDisabled={isDisabled}>Add group</Dialog.Button>
|
||||
<Dialog.Panel>
|
||||
<Dialog.Title>Add group</Dialog.Title>
|
||||
<Dialog.Text className="mb-4">
|
||||
Add this group to a list of allowed groups that can authenticate with
|
||||
Headscale via OIDC.
|
||||
</Dialog.Text>
|
||||
<input name="action_id" type="hidden" value="add_group" />
|
||||
<Input
|
||||
description="The group to allow for OIDC authentication."
|
||||
isInvalid={group.trim().length === 0 || isInvalid}
|
||||
isRequired
|
||||
label="Group"
|
||||
name="group"
|
||||
onChange={setGroup}
|
||||
placeholder="admin"
|
||||
/>
|
||||
{isInvalid && (
|
||||
<p className="text-red-500 text-sm mt-2">
|
||||
The group you entered already exists in the list of allowed groups.
|
||||
</p>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
return (
|
||||
<Dialog>
|
||||
<Button isDisabled={isDisabled}>Add group</Button>
|
||||
<DialogPanel>
|
||||
<Title>Add group</Title>
|
||||
<Text className="mb-4">
|
||||
Add this group to a list of allowed groups that can authenticate with Headscale via OIDC.
|
||||
</Text>
|
||||
<input name="action_id" type="hidden" value="add_group" />
|
||||
<Input
|
||||
description="The group to allow for OIDC authentication."
|
||||
isInvalid={group.trim().length === 0 || isInvalid}
|
||||
isRequired
|
||||
label="Group"
|
||||
name="group"
|
||||
onChange={setGroup}
|
||||
placeholder="admin"
|
||||
/>
|
||||
{isInvalid && (
|
||||
<p className="mt-2 text-sm text-red-500">
|
||||
The group you entered already exists in the list of allowed groups.
|
||||
</p>
|
||||
)}
|
||||
</DialogPanel>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,51 +1,54 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import Dialog from '~/components/Dialog';
|
||||
import Input from '~/components/Input';
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Input from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
import Title from "~/components/Title";
|
||||
|
||||
interface AddUserProps {
|
||||
users: string[];
|
||||
isDisabled?: boolean;
|
||||
users: string[];
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export default function AddUser({ users, isDisabled }: AddUserProps) {
|
||||
const [user, setUser] = useState('');
|
||||
const [user, setUser] = useState("");
|
||||
|
||||
const isInvalid = useMemo(() => {
|
||||
if (!user || user.trim().length === 0) {
|
||||
// Empty user is invalid, but no error shown
|
||||
return false;
|
||||
}
|
||||
const isInvalid = useMemo(() => {
|
||||
if (!user || user.trim().length === 0) {
|
||||
// Empty user is invalid, but no error shown
|
||||
return false;
|
||||
}
|
||||
|
||||
if (users.includes(user.trim())) {
|
||||
return true;
|
||||
}
|
||||
}, [user, users]);
|
||||
if (users.includes(user.trim())) {
|
||||
return true;
|
||||
}
|
||||
}, [user, users]);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog.Button isDisabled={isDisabled}>Add user</Dialog.Button>
|
||||
<Dialog.Panel>
|
||||
<Dialog.Title>Add user</Dialog.Title>
|
||||
<Dialog.Text className="mb-4">
|
||||
Add this user to a list of allowed users that can authenticate with
|
||||
Headscale via OIDC.
|
||||
</Dialog.Text>
|
||||
<input name="action_id" type="hidden" value="add_user" />
|
||||
<Input
|
||||
description="The user to allow for OIDC authentication."
|
||||
isInvalid={user.trim().length === 0 || isInvalid}
|
||||
isRequired
|
||||
label="User"
|
||||
name="user"
|
||||
onChange={setUser}
|
||||
placeholder="john_doe"
|
||||
/>
|
||||
{isInvalid && (
|
||||
<p className="text-red-500 text-sm mt-2">
|
||||
The user you entered already exists in the list of allowed users.
|
||||
</p>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Dialog>
|
||||
);
|
||||
return (
|
||||
<Dialog>
|
||||
<Button isDisabled={isDisabled}>Add user</Button>
|
||||
<DialogPanel>
|
||||
<Title>Add user</Title>
|
||||
<Text className="mb-4">
|
||||
Add this user to a list of allowed users that can authenticate with Headscale via OIDC.
|
||||
</Text>
|
||||
<input name="action_id" type="hidden" value="add_user" />
|
||||
<Input
|
||||
description="The user to allow for OIDC authentication."
|
||||
isInvalid={user.trim().length === 0 || isInvalid}
|
||||
isRequired
|
||||
label="User"
|
||||
name="user"
|
||||
onChange={setUser}
|
||||
placeholder="john_doe"
|
||||
/>
|
||||
{isInvalid && (
|
||||
<p className="mt-2 text-sm text-red-500">
|
||||
The user you entered already exists in the list of allowed users.
|
||||
</p>
|
||||
)}
|
||||
</DialogPanel>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GlobeLock, Group, User2 } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Form } from "react-router";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Button from "~/components/button";
|
||||
import TableList from "~/components/TableList";
|
||||
import cn from "~/utils/cn";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user