feat: swap button and decompose dialog components

This commit is contained in:
Aarnav Tale
2026-03-14 19:22:18 -04:00
parent eda5713700
commit 27f8fa0b42
40 changed files with 604 additions and 872 deletions
+10 -7
View File
@@ -1,5 +1,8 @@
import Dialog from "~/components/Dialog";
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 CreateUserProps {
isOidc?: boolean;
@@ -9,15 +12,15 @@ interface CreateUserProps {
export default function CreateUser({ isOidc, isDisabled }: CreateUserProps) {
return (
<Dialog>
<Dialog.Button isDisabled={isDisabled}>Add user</Dialog.Button>
<Dialog.Panel>
<Dialog.Title>Create a Headscale user</Dialog.Title>
<Dialog.Text className="mb-6">
<Button isDisabled={isDisabled}>Add user</Button>
<DialogPanel>
<Title>Create a Headscale user</Title>
<Text className="mb-6">
This creates a new user in Headscale. The user will appear in the &ldquo;Unlinked
Headscale Users&rdquo; section until they sign in
{isOidc ? " through your OIDC provider" : ""} and are automatically linked to a Headplane
account.
</Dialog.Text>
</Text>
<input name="action_id" type="hidden" value="create_user" />
<div className="flex flex-col gap-4">
<Input
@@ -54,7 +57,7 @@ export default function CreateUser({ isOidc, isDisabled }: CreateUserProps) {
validationBehavior="native"
/>
</div>
</Dialog.Panel>
</DialogPanel>
</Dialog>
);
}
+10 -8
View File
@@ -1,4 +1,6 @@
import Dialog from "~/components/Dialog";
import Dialog, { DialogPanel } from "~/components/Dialog";
import Text from "~/components/Text";
import Title from "~/components/Title";
import type { Machine, User } from "~/types";
interface DeleteProps {
@@ -13,15 +15,15 @@ export default function DeleteUser({ user, machines, isOpen, setIsOpen }: Delete
return (
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
<Dialog.Panel variant={machines.length > 0 ? "unactionable" : "normal"}>
<Dialog.Title>Delete {name}?</Dialog.Title>
<DialogPanel variant={machines.length > 0 ? "unactionable" : "normal"}>
<Title>Delete {name}?</Title>
{machines.length > 0 ? (
<Dialog.Text className="mb-6">
<Text className="mb-6">
Users cannot be deleted if they have machines. Please delete or re-assign their machines
to other users before proceeding.
</Dialog.Text>
</Text>
) : (
<Dialog.Text className="mb-6">
<Text className="mb-6">
Deleted users cannot be recovered.
{user.provider === "oidc" && (
<p className="mt-4 text-sm text-mist-600 dark:text-mist-300">
@@ -29,11 +31,11 @@ export default function DeleteUser({ user, machines, isOpen, setIsOpen }: Delete
they sign in again.
</p>
)}
</Dialog.Text>
</Text>
)}
<input name="action_id" type="hidden" value="delete_user" />
<input name="user_id" type="hidden" value={user.id} />
</Dialog.Panel>
</DialogPanel>
</Dialog>
);
}
+8 -6
View File
@@ -1,5 +1,7 @@
import Dialog from "~/components/Dialog";
import Dialog, { DialogPanel } from "~/components/Dialog";
import Notice from "~/components/Notice";
import Text from "~/components/Text";
import Title from "~/components/Title";
import cn from "~/utils/cn";
interface LinkUserProps {
@@ -21,12 +23,12 @@ export default function LinkUser({
}: LinkUserProps) {
return (
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
<Dialog.Panel>
<Dialog.Title>Link Headscale user for {displayName}</Dialog.Title>
<Dialog.Text className="mb-6">
<DialogPanel>
<Title>Link Headscale user for {displayName}</Title>
<Text className="mb-6">
Select which Headscale user this identity should be linked to. This controls which
machines they can manage and enables self-service features.
</Dialog.Text>
</Text>
{headscaleUsers.length === 0 ? (
<Notice>All Headscale users are already linked to other accounts.</Notice>
) : (
@@ -53,7 +55,7 @@ export default function LinkUser({
</select>
</>
)}
</Dialog.Panel>
</DialogPanel>
</Dialog>
);
}
+8 -6
View File
@@ -1,7 +1,9 @@
import Dialog from "~/components/Dialog";
import Dialog, { DialogPanel } from "~/components/Dialog";
import Link from "~/components/link";
import Notice from "~/components/Notice";
import RadioGroup from "~/components/RadioGroup";
import Text from "~/components/Text";
import Title from "~/components/Title";
import { Roles } from "~/server/web/roles";
import type { Role } from "~/server/web/roles";
@@ -22,15 +24,15 @@ export default function ReassignUser({
}: ReassignProps) {
return (
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
<Dialog.Panel variant={role === "owner" ? "unactionable" : "normal"}>
<Dialog.Title>Change role for {displayName}?</Dialog.Title>
<Dialog.Text className="mb-6">
<DialogPanel variant={role === "owner" ? "unactionable" : "normal"}>
<Title>Change role for {displayName}?</Title>
<Text className="mb-6">
Roles control what the user can access in Headplane. Each role grants a specific set of
capabilities.{" "}
<Link external styled to="https://tailscale.com/kb/1138/user-roles">
Learn More
</Link>
</Dialog.Text>
</Text>
{role === "owner" ? (
<Notice>The Tailnet owner cannot be reassigned.</Notice>
) : (
@@ -60,7 +62,7 @@ export default function ReassignUser({
</RadioGroup>
</>
)}
</Dialog.Panel>
</DialogPanel>
</Dialog>
);
}
+28 -27
View File
@@ -1,34 +1,35 @@
import Dialog from '~/components/Dialog';
import Input from '~/components/Input';
import { User } from '~/types';
import Dialog, { DialogPanel } from "~/components/Dialog";
import Input from "~/components/Input";
import Text from "~/components/Text";
import Title from "~/components/Title";
import { User } from "~/types";
interface RenameProps {
user: User;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
user: User;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
}
// TODO: Server side validation before submitting
export default function RenameUser({ user, isOpen, setIsOpen }: RenameProps) {
return (
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
<Dialog.Panel>
<Dialog.Title>Rename {user.name || user.displayName}?</Dialog.Title>
<Dialog.Text className="mb-6">
Enter a new username for {user.name || user.displayName}. Changing a
username will not update any ACL policies that may refer to this user
by their old username.
</Dialog.Text>
<input name="action_id" type="hidden" value="rename_user" />
<input name="user_id" type="hidden" value={user.id} />
<Input
defaultValue={user.name}
isRequired
label="Username"
name="new_name"
placeholder="my-new-name"
/>
</Dialog.Panel>
</Dialog>
);
return (
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
<DialogPanel>
<Title>Rename {user.name || user.displayName}?</Title>
<Text className="mb-6">
Enter a new username for {user.name || user.displayName}. Changing a username will not
update any ACL policies that may refer to this user by their old username.
</Text>
<input name="action_id" type="hidden" value="rename_user" />
<input name="user_id" type="hidden" value={user.id} />
<Input
defaultValue={user.name}
isRequired
label="Username"
name="new_name"
placeholder="my-new-name"
/>
</DialogPanel>
</Dialog>
);
}
@@ -1,5 +1,7 @@
import Dialog from "~/components/Dialog";
import Dialog, { DialogPanel } from "~/components/Dialog";
import Notice from "~/components/Notice";
import Text from "~/components/Text";
import Title from "~/components/Title";
interface TransferOwnershipProps {
targetUserId: string;
@@ -16,19 +18,19 @@ export default function TransferOwnership({
}: TransferOwnershipProps) {
return (
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
<Dialog.Panel variant="destructive">
<Dialog.Title>Transfer ownership to {targetDisplayName}?</Dialog.Title>
<Dialog.Text className="mb-6">
<DialogPanel variant="destructive">
<Title>Transfer ownership to {targetDisplayName}?</Title>
<Text className="mb-6">
This will make {targetDisplayName} the new owner of this Headplane instance. You will be
demoted to an Admin. This action cannot be easily undone.
</Dialog.Text>
</Text>
<Notice variant="warning">
Only the owner can transfer ownership. After this, you will no longer be able to manage
ownership.
</Notice>
<input name="action_id" type="hidden" value="transfer_ownership" />
<input name="user_id" type="hidden" value={targetUserId} />
</Dialog.Panel>
</DialogPanel>
</Dialog>
);
}