Files
headplane/app/routes/users/dialogs/link-user.tsx
T
Aarnav Tale 434f886034 feat: admin UI for Headscale user linking + skip onboarding
- Add 'Link Headscale user' option in user menu (admin-only, OIDC users)
- Dialog shows only unclaimed Headscale users
- New link_user action in user-actions with claim validation
- Onboarding now allows skipping the link step with clear messaging
- Users who skip are told they can ask an admin to link later

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019cce57-c9e1-7732-9709-8288127573a9
2026-03-08 17:34:57 -04:00

59 lines
1.9 KiB
TypeScript

import Dialog from "~/components/Dialog";
import Notice from "~/components/Notice";
import type { User } from "~/types";
import cn from "~/utils/cn";
interface LinkUserProps {
user: User & { headplaneRole: string };
headscaleUsers: { id: string; name: string }[];
currentLink?: string;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
}
export default function LinkUser({
user,
headscaleUsers,
currentLink,
isOpen,
setIsOpen,
}: LinkUserProps) {
return (
<Dialog isOpen={isOpen} onOpenChange={setIsOpen}>
<Dialog.Panel>
<Dialog.Title>Link Headscale user for {user.name || user.displayName}</Dialog.Title>
<Dialog.Text className="mb-6">
Select which Headscale user this OIDC identity should be linked to. This controls which
machines they can manage and enables self-service features.
</Dialog.Text>
{headscaleUsers.length === 0 ? (
<Notice>All Headscale users are already linked to other accounts.</Notice>
) : (
<>
<input name="action_id" type="hidden" value="link_user" />
<input name="user_id" type="hidden" value={user.id} />
<select
className={cn(
"w-full rounded-lg border p-2",
"border-headplane-200 dark:border-headplane-700",
"bg-headplane-50 dark:bg-headplane-900",
)}
defaultValue={currentLink ?? ""}
name="headscale_user_id"
required
>
<option value="">Select a Headscale user...</option>
{headscaleUsers.map((u) => (
<option key={u.id} value={u.id}>
{u.name}
{u.id === currentLink ? " (current)" : ""}
</option>
))}
</select>
</>
)}
</Dialog.Panel>
</Dialog>
);
}