mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-14 17:06:39 +00:00
show owner in users table, list roles query in invite form, and more
This commit is contained in:
@@ -29,7 +29,7 @@ export function TopbarNav({
|
||||
className={cn(
|
||||
"flex overflow-x-auto space-x-4 lg:space-x-6",
|
||||
disabled && "opacity-50 pointer-events-none",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -38,22 +38,23 @@ export function TopbarNav({
|
||||
key={item.href}
|
||||
href={item.href.replace("{orgId}", orgId)}
|
||||
className={cn(
|
||||
"px-2 py-3 text-md",
|
||||
"relative px-3 py-3 text-md",
|
||||
pathname.startsWith(item.href.replace("{orgId}", orgId))
|
||||
? "border-b-2 border-primary text-primary font-medium"
|
||||
: "hover:text-primary text-muted-foreground font-medium",
|
||||
"whitespace-nowrap",
|
||||
disabled && "cursor-not-allowed",
|
||||
disabled && "cursor-not-allowed"
|
||||
)}
|
||||
onClick={disabled ? (e) => e.preventDefault() : undefined}
|
||||
tabIndex={disabled ? -1 : undefined}
|
||||
aria-disabled={disabled}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 relative px-2 py-0.5 rounded-md">
|
||||
{item.icon && (
|
||||
<div className="hidden md:block">{item.icon}</div>
|
||||
)}
|
||||
{item.title}
|
||||
<span className="relative z-10">{item.title}</span>
|
||||
<span className="absolute inset-x-0 bottom-0 border-b-2 border-transparent group-hover:border-primary"></span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -86,7 +86,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full bg-muted mb-6 select-none sm:px-0 px-3 pt-3">
|
||||
<div className="w-full border-b bg-neutral-100 dark:bg-neutral-900 mb-6 select-none sm:px-0 px-3 pt-3">
|
||||
<div className="container mx-auto flex flex-col content-between gap-4 ">
|
||||
<Header
|
||||
email={user.email}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useParams } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Checkbox } from "@app/components/ui/checkbox";
|
||||
import { PickSiteDefaultsResponse } from "@server/routers/site";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
|
||||
const method = [
|
||||
{ label: "Wireguard", value: "wg" },
|
||||
@@ -188,19 +189,11 @@ sh get-docker.sh`;
|
||||
)}
|
||||
/>
|
||||
{form.watch("method") === "wg" && !isLoading ? (
|
||||
<pre className="mt-2 w-full rounded-md bg-muted p-4 overflow-x-auto">
|
||||
<code className="whitespace-pre-wrap font-mono">
|
||||
{wgConfig}
|
||||
</code>
|
||||
</pre>
|
||||
<CopyTextBox text={wgConfig} />
|
||||
) : form.watch("method") === "wg" && isLoading ? (
|
||||
<p>Loading WireGuard configuration...</p>
|
||||
) : (
|
||||
<pre className="mt-2 w-full rounded-md bg-muted p-4 overflow-x-auto">
|
||||
<code className="whitespace-pre-wrap">
|
||||
{newtConfig}
|
||||
</code>
|
||||
</pre>
|
||||
<CopyTextBox text={newtConfig} wrapText={false} />
|
||||
)}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useToast } from "@app/hooks/useToast";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { InviteUserBody, InviteUserResponse } from "@server/routers/user";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
CredenzaTitle,
|
||||
} from "@app/components/Credenza";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { ListRolesResponse } from "@server/routers/role";
|
||||
|
||||
type InviteUserFormProps = {
|
||||
open: boolean;
|
||||
@@ -45,8 +46,8 @@ type InviteUserFormProps = {
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.string().email({ message: "Invalid email address" }),
|
||||
validForHours: z.string(),
|
||||
roleId: z.string(),
|
||||
validForHours: z.string().min(1, { message: "Please select a duration" }),
|
||||
roleId: z.string().min(1, { message: "Please select a role" }),
|
||||
});
|
||||
|
||||
export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||
@@ -57,7 +58,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [expiresInDays, setExpiresInDays] = useState(1);
|
||||
|
||||
const roles = [{ roleId: 1, name: "Admin" }];
|
||||
const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]);
|
||||
|
||||
const validFor = [
|
||||
{ hours: 24, name: "1 day" },
|
||||
@@ -73,11 +74,44 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
validForHours: "168",
|
||||
roleId: "4",
|
||||
validForHours: "72",
|
||||
roleId: "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
async function fetchRoles() {
|
||||
const res = await api
|
||||
.get<AxiosResponse<ListRolesResponse>>(
|
||||
`/org/${org?.org.orgId}/roles`
|
||||
)
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to fetch roles",
|
||||
description:
|
||||
e.message ||
|
||||
"An error occurred while fetching the roles",
|
||||
});
|
||||
});
|
||||
|
||||
if (res?.status === 200) {
|
||||
setRoles(res.data.data.roles);
|
||||
// form.setValue(
|
||||
// "roleId",
|
||||
// res.data.data.roles[0].roleId.toString()
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
fetchRoles();
|
||||
}, [open]);
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
setLoading(true);
|
||||
|
||||
@@ -167,7 +201,6 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
defaultValue={field.value.toString()}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
|
||||
@@ -8,11 +8,10 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||
import { ArrowUpDown, Crown, MoreHorizontal } from "lucide-react";
|
||||
import { UsersDataTable } from "./UsersDataTable";
|
||||
import { useState } from "react";
|
||||
import InviteUserForm from "./InviteUserForm";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
import api from "@app/api";
|
||||
@@ -24,6 +23,7 @@ export type UserRow = {
|
||||
email: string;
|
||||
status: string;
|
||||
role: string;
|
||||
isOwner: boolean;
|
||||
};
|
||||
|
||||
type UsersTableProps = {
|
||||
@@ -87,6 +87,16 @@ export default function UsersTable({ users }: UsersTableProps) {
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const userRow = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{userRow.isOwner && <Crown className="w-4 h-4" />}
|
||||
<span>{userRow.role}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
@@ -95,30 +105,39 @@ export default function UsersTable({ users }: UsersTableProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>Manage user</DropdownMenuItem>
|
||||
{userRow.email !== user?.email && (
|
||||
{!userRow.isOwner && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Open menu
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<button
|
||||
className="text-red-600 hover:text-red-800"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setUserToRemove(userRow);
|
||||
}}
|
||||
>
|
||||
Remove User
|
||||
</button>
|
||||
Manage user
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{userRow.email !== user?.email && (
|
||||
<DropdownMenuItem>
|
||||
<button
|
||||
className="text-red-600 hover:text-red-800"
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
setUserToRemove(userRow);
|
||||
}}
|
||||
>
|
||||
Remove User
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import { cache } from "react";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import UserProvider from "@app/providers/UserProvider";
|
||||
import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
|
||||
type UsersPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -55,7 +56,8 @@ export default async function UsersPage(props: UsersPageProps) {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
status: "Confirmed",
|
||||
role: user.roleName || "",
|
||||
role: user.isOwner ? "Owner" : user.roleName || "Member",
|
||||
isOwner: user.isOwner || false,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user