feat(admin): users and roles added to the frontend to manage easier
This commit is contained in:
@@ -1,81 +1,81 @@
|
||||
import {
|
||||
createRootRouteWithContext,
|
||||
Outlet,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { Toaster } from "sonner";
|
||||
import Cookies from "js-cookie";
|
||||
import { SessionGuard } from "../lib/providers/SessionProvider";
|
||||
import Nav from "../components/navBar/Nav";
|
||||
import { ThemeProvider } from "../lib/providers/theme-provider";
|
||||
import { SidebarProvider } from "../components/ui/sidebar";
|
||||
import SideBarNav from "../components/navBar/SideBarNav";
|
||||
import {
|
||||
createRootRouteWithContext,
|
||||
Outlet,
|
||||
useRouter,
|
||||
} from "@tanstack/react-router";
|
||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||
import { userAccess } from "../lib/authClient";
|
||||
import mobile from "is-mobile";
|
||||
import Cookies from "js-cookie";
|
||||
import { useEffect } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import Nav from "../components/navBar/Nav";
|
||||
import SideBarNav from "../components/navBar/SideBarNav";
|
||||
import { SidebarProvider, SidebarTrigger } from "../components/ui/sidebar";
|
||||
import { userAccess } from "../lib/authClient";
|
||||
import { SessionGuard } from "../lib/providers/SessionProvider";
|
||||
import { ThemeProvider } from "../lib/providers/theme-provider";
|
||||
import { coreSocket } from "../lib/socket.io/socket";
|
||||
|
||||
interface RootRouteContext {
|
||||
queryClient: QueryClient;
|
||||
//user: User | null;
|
||||
//login: (user: User) => void;
|
||||
//logout: () => void;
|
||||
queryClient: QueryClient;
|
||||
//user: User | null;
|
||||
//login: (user: User) => void;
|
||||
//logout: () => void;
|
||||
}
|
||||
|
||||
const RootLayout = () => {
|
||||
//const { logout, login } = Route.useRouteContext();
|
||||
const defaultOpen = Cookies.get("sidebar_state") === "true";
|
||||
const router = useRouter();
|
||||
// console.log(mobile({ featureDetect: true, tablet: true }));
|
||||
//const { logout, login } = Route.useRouteContext();
|
||||
const defaultOpen = Cookies.get("sidebar_state") === "true";
|
||||
const router = useRouter();
|
||||
// console.log(mobile({ featureDetect: true, tablet: true }));
|
||||
|
||||
// if mobile lets move to the mobile section.
|
||||
useEffect(() => {
|
||||
if (mobile({ featureDetect: true, tablet: true })) {
|
||||
router.navigate({ to: "/m" });
|
||||
}
|
||||
// if mobile lets move to the mobile section.
|
||||
useEffect(() => {
|
||||
if (mobile({ featureDetect: true, tablet: true })) {
|
||||
router.navigate({ to: "/m" });
|
||||
}
|
||||
|
||||
coreSocket.on("connect", () => {
|
||||
console.log("✅ Connected:", coreSocket.id);
|
||||
});
|
||||
coreSocket.on("connect", () => {
|
||||
console.log("✅ Connected:", coreSocket.id);
|
||||
});
|
||||
|
||||
coreSocket.on("disconnect", () => {
|
||||
console.log("🔴 Disconnected");
|
||||
});
|
||||
coreSocket.on("disconnect", () => {
|
||||
console.log("🔴 Disconnected");
|
||||
});
|
||||
|
||||
return () => {
|
||||
coreSocket.off("connect");
|
||||
coreSocket.off("disconnect");
|
||||
};
|
||||
}, []);
|
||||
return () => {
|
||||
coreSocket.off("connect");
|
||||
coreSocket.off("disconnect");
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SessionGuard>
|
||||
<ThemeProvider>
|
||||
<div className="flex flex-col h-screen overflow-hidden">
|
||||
<Nav />
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<SidebarProvider defaultOpen={defaultOpen}>
|
||||
<SideBarNav />
|
||||
<div className="flex-2 overflow-y-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
<Toaster expand richColors closeButton />
|
||||
{userAccess(null, ["systemAdmin"]) && (
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
)}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</SessionGuard>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<SessionGuard>
|
||||
<ThemeProvider>
|
||||
<div className="flex flex-col h-screen overflow-hidden">
|
||||
<Nav />
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<SidebarProvider defaultOpen={defaultOpen}>
|
||||
<SideBarNav />
|
||||
|
||||
<div className="flex-2 overflow-y-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
<Toaster expand richColors closeButton />
|
||||
{userAccess(null, ["systemAdmin"]) && (
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
)}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</SessionGuard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createRootRouteWithContext<RootRouteContext>()({
|
||||
component: RootLayout,
|
||||
component: RootLayout,
|
||||
});
|
||||
|
||||
144
frontend/src/routes/_adminLayout/-components/ExpandedRow.tsx
Normal file
144
frontend/src/routes/_adminLayout/-components/ExpandedRow.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../../components/ui/select";
|
||||
import { api } from "../../../lib/axiosAPI";
|
||||
|
||||
const modules: string[] = [
|
||||
"users",
|
||||
"system",
|
||||
"ocp",
|
||||
"siloAdjustments",
|
||||
"demandManagement",
|
||||
"logistics",
|
||||
"production",
|
||||
"quality",
|
||||
"eom",
|
||||
"forklifts",
|
||||
];
|
||||
|
||||
const roles: string[] = [
|
||||
"systemAdmin",
|
||||
"admin",
|
||||
"manager",
|
||||
"supervisor",
|
||||
"tester",
|
||||
"user",
|
||||
"viewer",
|
||||
];
|
||||
|
||||
export default function ExpandedRow({ row }: { row: any }) {
|
||||
const user = row.original;
|
||||
const existingRolesMap = Object.fromEntries(
|
||||
(user.roles || []).map((r: { module: string; role: string }) => [
|
||||
r.module,
|
||||
r.role,
|
||||
]),
|
||||
);
|
||||
|
||||
// local state for selections
|
||||
const [selectedRoles, setSelectedRoles] = useState<Record<string, string>>(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
modules.map((m) => [m, existingRolesMap[m] || "viewer"]),
|
||||
),
|
||||
);
|
||||
|
||||
const onSubmitRole = async (module: string) => {
|
||||
const role = selectedRoles[module];
|
||||
// console.log("Saving module role:", {
|
||||
// module,
|
||||
// role,
|
||||
// user,
|
||||
// });
|
||||
try {
|
||||
const result = await api.patch(`/api/admin/${user.id}/grant`, {
|
||||
module: module,
|
||||
role: role,
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
toast.success(
|
||||
`${user.username} was just granted ${role} on module ${module}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error(
|
||||
"There was an error granting the user a role if this continues please contact your admin.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectChange = (module: string, value: string) => {
|
||||
setSelectedRoles((prev) => ({ ...prev, [module]: value }));
|
||||
};
|
||||
|
||||
const onDeleteRole = async (module: string) => {
|
||||
try {
|
||||
const result = await api.patch(`/api/admin/${user.id}/revoke`, {
|
||||
module: module,
|
||||
});
|
||||
|
||||
if (result.status === 200) {
|
||||
toast.success(`${user.username} no longer has access to ${module}`);
|
||||
setSelectedRoles((prev) => ({ ...prev, [module]: "viewer" }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error(
|
||||
"There was an error granting the user a role if this continues please contact your admin.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-muted w-128">
|
||||
<div className="">
|
||||
{modules.map((i) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex flex-row items-center justify-between gap-4 border-b py-2"
|
||||
>
|
||||
<span className="text-sm font-medium capitalize w-40">{i}</span>
|
||||
|
||||
<Select
|
||||
value={selectedRoles[i]}
|
||||
onValueChange={(v) => handleSelectChange(i, v)}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Select role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles.map((r) => (
|
||||
<SelectItem key={r} value={r}>
|
||||
{r}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button onClick={() => onSubmitRole(i)} size="sm">
|
||||
Grant
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onDeleteRole(i)}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
>
|
||||
Revoke
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,215 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Mail,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../../../components/ui/table";
|
||||
import { getUsers } from "../../../../lib/querys/admin/getUsers";
|
||||
import ExpandedRow from "../../-components/ExpandedRow";
|
||||
|
||||
type User = {
|
||||
username: string;
|
||||
email: string;
|
||||
roles: string | null;
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_adminLayout/admin/_users/users")({
|
||||
component: RouteComponent,
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div className="">Hello "/_admin/admin/users "!</div>;
|
||||
const { data, isLoading } = useQuery(getUsers());
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const columnHelper = createColumnHelper<User>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor("username", {
|
||||
cell: (i) => i.getValue(),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
<span className="flex flex-row gap-2">
|
||||
<User />
|
||||
Username
|
||||
</span>
|
||||
{column.getIsSorted() === "asc" ? (
|
||||
<ArrowUp className="ml-2 h-4 w-4" />
|
||||
) : (
|
||||
<ArrowDown className="ml-2 h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
}),
|
||||
|
||||
columnHelper.accessor("email", {
|
||||
cell: (i) => i.getValue(),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
<span className="flex flex-row gap-2">
|
||||
<Mail />
|
||||
Email
|
||||
</span>
|
||||
{column.getIsSorted() === "asc" ? (
|
||||
<ArrowUp className="ml-2 h-4 w-4" />
|
||||
) : (
|
||||
<ArrowDown className="ml-2 h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("roles", {
|
||||
header: () => <span>Roles</span>,
|
||||
cell: ({ row }) => {
|
||||
return row.getCanExpand() ? (
|
||||
<button
|
||||
{...{
|
||||
onClick: row.getToggleExpandedHandler(),
|
||||
style: { cursor: "pointer" },
|
||||
}}
|
||||
>
|
||||
{row.getIsExpanded() ? (
|
||||
<span className="flex flex-row gap-2">
|
||||
Roles <ChevronDown />
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex flex-row gap-2">
|
||||
Roles <ChevronRight />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
"No expanding"
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
//renderSubComponent: ({ row }: { row: any }) => <ExpandedRow row={row} />,
|
||||
getRowCanExpand: () => true,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="m-auto">Loading user data</div>;
|
||||
}
|
||||
|
||||
// render the roles card and make ts happy by not including it in the useReactTable hook
|
||||
const renderSubComponent = ({ row }: { row: any }) => (
|
||||
<ExpandedRow row={row} />
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<React.Fragment key={row.id}>
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
{row.getIsExpanded() && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={row.getVisibleCells().length}>
|
||||
{renderSubComponent({ row })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user