diff --git a/app/routes/users/components/headplane-user-row.tsx b/app/routes/users/components/headplane-user-row.tsx new file mode 100644 index 0000000..e38c92b --- /dev/null +++ b/app/routes/users/components/headplane-user-row.tsx @@ -0,0 +1,95 @@ +import { CircleUser } from "lucide-react"; + +import StatusCircle from "~/components/StatusCircle"; +import type { Role } from "~/server/web/roles"; +import cn from "~/utils/cn"; + +import type { HeadplaneUserData } from "../overview"; +import MenuOptions from "./menu"; + +interface HeadplaneUserRowProps { + user: HeadplaneUserData; + headscaleUsers: { id: string; name: string; claimed: boolean }[]; +} + +export default function HeadplaneUserRow({ user, headscaleUsers }: HeadplaneUserRowProps) { + const isOnline = user.machines.some((machine) => machine.online); + const lastSeen = user.machines.reduce( + (acc, machine) => Math.max(acc, new Date(machine.lastSeen).getTime()), + 0, + ); + + const displayName = user.linkedHeadscaleUser?.displayName || user.name || user.email || user.sub; + const displayEmail = user.linkedHeadscaleUser?.email ?? user.email; + + return ( +
{displayName}
+ {displayEmail &&{displayEmail}
} + {!user.headscaleUserId && ( +Not linked
+ )} +{mapRoleToName(user.role)}
++ {user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleDateString() : "Never"} +
++ {isOnline ? "Connected" : new Date(lastSeen).toLocaleString()} +
+ + ) : ( +No machines
+ )} +Member
; + default: + return "Unknown"; + } +} diff --git a/app/routes/users/components/headscale-user-row.tsx b/app/routes/users/components/headscale-user-row.tsx new file mode 100644 index 0000000..164d54d --- /dev/null +++ b/app/routes/users/components/headscale-user-row.tsx @@ -0,0 +1,62 @@ +import { CircleUser } from "lucide-react"; + +import StatusCircle from "~/components/StatusCircle"; +import cn from "~/utils/cn"; + +import type { UnlinkedHeadscaleUser } from "../overview"; + +interface HeadscaleUserRowProps { + user: UnlinkedHeadscaleUser; +} + +export default function HeadscaleUserRow({ user }: HeadscaleUserRowProps) { + const isOnline = user.machines.some((machine) => machine.online); + const lastSeen = user.machines.reduce( + (acc, machine) => Math.max(acc, new Date(machine.lastSeen).getTime()), + 0, + ); + + return ( +{user.name || user.displayName}
+ {user.email &&{user.email}
} ++ {new Date(user.createdAt).toLocaleDateString()} +
++ {isOnline ? "Connected" : new Date(lastSeen).toLocaleString()} +
+ + ) : ( +No machines
+ )} +- {oidc ? ( - <> - Users are managed through your{" "} - - OpenID Connect provider - - {". "} - Groups and user information do not automatically sync.{" "} - Learn more - > - ) : ( - <> - Users are not managed externally. Using OpenID Connect can create a better - experience when using Headscale.{" "} - Learn more - > - )} -
-- {oidc - ? "You can still add users manually, however it is recommended that you manage users through your OIDC provider." - : "You can add, remove, and rename users here."} -
-+ {oidc ? ( + <> + Users are managed through your{" "} + + OIDC provider + + . + > + ) : ( + <> + Users are managed locally.{" "} + + Set up OIDC + + > + )} +
{user.name || user.displayName}
-{user.email}
-{mapRoleToName(role)}
-- {new Date(user.createdAt).toLocaleDateString()} -
-- {isOnline ? "Connected" : new Date(lastSeen).toLocaleString()} -
- -Unmanaged
; - case "invalid-oidc": - returnInvalid
; - case "no-role": - returnUnregistered
; - case "owner": - return "Owner"; - case "admin": - return "Admin"; - case "network_admin": - return "Network Admin"; - case "it_admin": - return "IT Admin"; - case "auditor": - return "Auditor"; - case "member": - returnNo Access
; - default: - return "Unknown"; - } -} diff --git a/app/routes/users/dialogs/create-user.tsx b/app/routes/users/dialogs/create-user.tsx index f7d53ca..a5e29f8 100644 --- a/app/routes/users/dialogs/create-user.tsx +++ b/app/routes/users/dialogs/create-user.tsx @@ -1,66 +1,60 @@ -import Dialog from '~/components/Dialog'; -import Input from '~/components/Input'; +import Dialog from "~/components/Dialog"; +import Input from "~/components/Input"; interface CreateUserProps { - isOidc?: boolean; - isDisabled?: boolean; + isOidc?: boolean; + isDisabled?: boolean; } -// TODO: Support image upload for user avatars export default function CreateUser({ isOidc, isDisabled }: CreateUserProps) { - return ( - + ); } diff --git a/app/routes/users/dialogs/delete-user.tsx b/app/routes/users/dialogs/delete-user.tsx index 14d2825..1db2082 100644 --- a/app/routes/users/dialogs/delete-user.tsx +++ b/app/routes/users/dialogs/delete-user.tsx @@ -1,20 +1,21 @@ import Dialog from "~/components/Dialog"; -import { Machine, User } from "~/types"; +import type { Machine, User } from "~/types"; interface DeleteProps { - user: User & { machines: Machine[] }; + user: User; + machines: Machine[]; isOpen: boolean; setIsOpen: (isOpen: boolean) => void; } -export default function DeleteUser({ user, isOpen, setIsOpen }: DeleteProps) { +export default function DeleteUser({ user, machines, isOpen, setIsOpen }: DeleteProps) { const name = user.name || user.displayName; return (