diff --git a/LogisticsSupportTool_API_DOCS/app/admin/User/GrantROle by ID.bru b/LogisticsSupportTool_API_DOCS/app/admin/User/GrantROle by ID.bru index 8cbd46e..faad09c 100644 --- a/LogisticsSupportTool_API_DOCS/app/admin/User/GrantROle by ID.bru +++ b/LogisticsSupportTool_API_DOCS/app/admin/User/GrantROle by ID.bru @@ -4,23 +4,24 @@ meta { seq: 7 } -post { +patch { url: {{url}}/lst/api/admin/:userID/grant body: json auth: inherit } params:path { - userID: 0hlO48C7Jw1J804FxrCnonK + userID: 0hlO48C7Jw1J804FxrCnonKjQ2zh48R6 } body:json { { - "module":"users", - "role":"admin" + "module":"siloAdjustments", + "role":"viewer" } } settings { encodeUrl: true + timeout: 0 } diff --git a/LogisticsSupportTool_API_DOCS/app/admin/User/RevokeRole by ID.bru b/LogisticsSupportTool_API_DOCS/app/admin/User/RevokeRole by ID.bru new file mode 100644 index 0000000..ef58d43 --- /dev/null +++ b/LogisticsSupportTool_API_DOCS/app/admin/User/RevokeRole by ID.bru @@ -0,0 +1,27 @@ +meta { + name: RevokeRole by ID + type: http + seq: 3 +} + +post { + url: {{url}}/lst/api/admin/:userID/grant + body: json + auth: inherit +} + +params:path { + userID: 0hlO48C7Jw1J804FxrCnonKjQ2zh48R6 +} + +body:json { + { + "module":"siloAdjustments", + "role":"viewer" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/app/src/internal/admin/routes.ts b/app/src/internal/admin/routes.ts index 7e64748..4386acd 100644 --- a/app/src/internal/admin/routes.ts +++ b/app/src/internal/admin/routes.ts @@ -4,22 +4,30 @@ import { requireAuth } from "../../pkg/middleware/authMiddleware.js"; //admin routes import users from "./routes/getUserRoles.js"; import grantRoles from "./routes/grantRole.js"; +import revokeRoles from "./routes/revokeRole.js"; import servers from "./routes/servers/serverRoutes.js"; export const setupAdminRoutes = (app: Express, basePath: string) => { - app.use( - basePath + "/api/admin/server", // will pass bc system admin but this is just telling us we need this - servers - ); + app.use( + basePath + "/api/admin/server", // will pass bc system admin but this is just telling us we need this + servers, + ); - app.use( - basePath + "/api/admin/users", - requireAuth("user", ["systemAdmin"]), // will pass bc system admin but this is just telling us we need this - users - ); - app.use( - basePath + "/api/admin", - requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this - grantRoles - ); + app.use( + basePath + "/api/admin/users", + requireAuth("user", ["systemAdmin"]), // will pass bc system admin but this is just telling us we need this + users, + ); + + app.use( + basePath + "/api/admin", + requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this + grantRoles, + ); + + app.use( + basePath + "/api/admin", + requireAuth("user", ["systemAdmin", "admin"]), // will pass bc system admin but this is just telling us we need this + revokeRoles, + ); }; diff --git a/app/src/internal/admin/routes/grantRole.ts b/app/src/internal/admin/routes/grantRole.ts index 2d442b3..797082c 100644 --- a/app/src/internal/admin/routes/grantRole.ts +++ b/app/src/internal/admin/routes/grantRole.ts @@ -1,74 +1,82 @@ -import { Router } from "express"; import type { Request, Response } from "express"; -import { tryCatch } from "../../../pkg/utils/tryCatch.js"; -import { db } from "../../../pkg/db/db.js"; +import { Router } from "express"; import z from "zod"; +import { db } from "../../../pkg/db/db.js"; import { userRoles } from "../../../pkg/db/schema/user_roles.js"; import { createLogger } from "../../../pkg/logger/logger.js"; +import { tryCatch } from "../../../pkg/utils/tryCatch.js"; const roleSchema = z.object({ - module: z.enum([ - "users", - "system", - "ocp", - "siloAdjustments", - "demandManagement", - "logistics", - "production", - "quality", - "eom", - "forklifts", - ]), - role: z.enum(["admin", "manager", "supervisor", "test,", "viewer"]), + module: z.enum([ + "users", + "system", + "ocp", + "siloAdjustments", + "demandManagement", + "logistics", + "production", + "quality", + "eom", + "forklifts", + ]), + role: z.enum([ + "systemAdmin", + "admin", + "manager", + "supervisor", + "tester", + "user", + "viewer", + ]), }); const router = Router(); -router.post("/:userId/grant", async (req: Request, res: Response) => { - const log = createLogger({ - module: "admin", - subModule: "grantRoles", - }); - const userId = req.params.userId; - console.log(userId); +router.patch("/:userId/grant", async (req: Request, res: Response) => { + const log = createLogger({ + module: "admin", + subModule: "grantRoles", + }); + const userId = req.params.userId; - try { - const validated = roleSchema.parse(req.body); + try { + const validated = roleSchema.parse(req.body); - const data = await db - .insert(userRoles) - .values({ - userId, - module: validated.module, - role: validated.role, - }) - .onConflictDoUpdate({ - target: [userRoles.userId, userRoles.module], - set: { module: validated.module, role: validated.role }, - }); - log.info( - {}, - `Module: ${validated.module}, Role: ${validated.role} as was just granted to userID: ${userId}` - ); - return res.status(200).json({ - success: true, - message: `Module: ${validated.module}, Role: ${validated.role} as was just granted`, - data, - }); - } catch (err) { - if (err instanceof z.ZodError) { - const flattened = z.flattenError(err); - return res.status(400).json({ - error: "Validation failed", - details: flattened, - }); - } + const data = await db + .insert(userRoles) + .values({ + userId: userId, + module: validated.module, + role: validated.role, + }) + .onConflictDoUpdate({ + target: [userRoles.userId, userRoles.module], + set: { module: validated.module, role: validated.role }, + }); + log.info( + {}, + `Module: ${validated.module}, Role: ${validated.role} as was just granted to userID: ${userId}`, + ); + return res.status(200).json({ + success: true, + message: `Module: ${validated.module}, Role: ${validated.role} as was just granted`, + data, + }); + } catch (err) { + if (err instanceof z.ZodError) { + const flattened = z.flattenError(err); + return res.status(400).json({ + error: "Validation failed", + details: flattened, + }); + } - return res.status(400).json({ - success: false, - message: "Invalid input please try again.", - }); - } + return res.status(400).json({ + success: false, + message: "Invalid input please try again.", + error: err, + }); + } }); export default router; diff --git a/app/src/internal/admin/routes/revokeRole.ts b/app/src/internal/admin/routes/revokeRole.ts new file mode 100644 index 0000000..c40ca2a --- /dev/null +++ b/app/src/internal/admin/routes/revokeRole.ts @@ -0,0 +1,71 @@ +import { and, eq } from "drizzle-orm"; +import type { Request, Response } from "express"; +import { Router } from "express"; +import z from "zod"; +import { db } from "../../../pkg/db/db.js"; +import { userRoles } from "../../../pkg/db/schema/user_roles.js"; +import { createLogger } from "../../../pkg/logger/logger.js"; +import { tryCatch } from "../../../pkg/utils/tryCatch.js"; + +const roleSchema = z.object({ + module: z.enum([ + "users", + "system", + "ocp", + "siloAdjustments", + "demandManagement", + "logistics", + "production", + "quality", + "eom", + "forklifts", + ]), +}); + +const router = Router(); + +router.patch("/:userId/revoke", async (req: Request, res: Response) => { + const log = createLogger({ + module: "admin", + subModule: "grantRoles", + }); + const userId = req.params.userId; + + try { + const validated = roleSchema.parse(req.body); + + const data = await db + .delete(userRoles) + .where( + and( + eq(userRoles.userId, userId), + eq(userRoles.module, validated.module), + ), + ); + log.info( + {}, + `Module: ${validated.module}, was just revoked fron userID: ${userId}`, + ); + return res.status(200).json({ + success: true, + message: `Module: ${validated.module}, was just revoked fron userID: ${userId}`, + data, + }); + } catch (err) { + if (err instanceof z.ZodError) { + const flattened = z.flattenError(err); + return res.status(400).json({ + error: "Validation failed", + details: flattened, + }); + } + + return res.status(400).json({ + success: false, + message: "Invalid input please try again.", + error: err, + }); + } +}); + +export default router; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 01b2379..8940676 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,6 +25,7 @@ "@tanstack/react-query": "^5.89.0", "@tanstack/react-router": "^1.131.36", "@tanstack/react-router-devtools": "^1.131.36", + "@tanstack/react-table": "^8.21.3", "@types/react-calendar-timeline": "^0.28.6", "axios": "^1.12.2", "better-auth": "^1.3.11", @@ -3372,6 +3373,26 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@tanstack/router-core": { "version": "1.131.36", "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.131.36.tgz", @@ -3533,6 +3554,19 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/virtual-file-routes": { "version": "1.131.2", "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.131.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 143aaa4..c8e65eb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "@tanstack/react-query": "^5.89.0", "@tanstack/react-router": "^1.131.36", "@tanstack/react-router-devtools": "^1.131.36", + "@tanstack/react-table": "^8.21.3", "@types/react-calendar-timeline": "^0.28.6", "axios": "^1.12.2", "better-auth": "^1.3.11", diff --git a/frontend/src/components/navBar/Nav.tsx b/frontend/src/components/navBar/Nav.tsx index 0d1d574..d867e8a 100644 --- a/frontend/src/components/navBar/Nav.tsx +++ b/frontend/src/components/navBar/Nav.tsx @@ -1,81 +1,76 @@ import { Link } from "@tanstack/react-router"; import { useAuth, useLogout } from "../../lib/authClient"; -import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; import { ModeToggle } from "../mode-toggle"; +import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { Button } from "../ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; export default function Nav() { - const { session } = useAuth(); - const logout = useLogout(); - return ( - + ); } diff --git a/frontend/src/components/navBar/SideBarNav.tsx b/frontend/src/components/navBar/SideBarNav.tsx index e48e76c..9dbf960 100644 --- a/frontend/src/components/navBar/SideBarNav.tsx +++ b/frontend/src/components/navBar/SideBarNav.tsx @@ -1,26 +1,26 @@ -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarTrigger, -} from "../ui/sidebar"; -import { Header } from "./Header"; -import Admin from "./Admin"; import { userAccess } from "../../lib/authClient"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarTrigger, +} from "../ui/sidebar"; +import Admin from "./Admin"; +import { Header } from "./Header"; export default function SideBarNav() { - return ( -
- -
- - {userAccess(null, ["systemAdmin", "admin"]) && } - + return ( +
+ +
+ + {userAccess(null, ["systemAdmin", "admin"]) && } + - - - - -
- ); + + + + +
+ ); } diff --git a/frontend/src/components/ui/table.tsx b/frontend/src/components/ui/table.tsx new file mode 100644 index 0000000..5513a5c --- /dev/null +++ b/frontend/src/components/ui/table.tsx @@ -0,0 +1,114 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/frontend/src/lib/querys/admin/getUsers.ts b/frontend/src/lib/querys/admin/getUsers.ts new file mode 100644 index 0000000..2fe73c8 --- /dev/null +++ b/frontend/src/lib/querys/admin/getUsers.ts @@ -0,0 +1,17 @@ +import { queryOptions } from "@tanstack/react-query"; +import axios from "axios"; + +export function getUsers() { + return queryOptions({ + queryKey: ["getUsers"], + queryFn: () => fetchSession(), + staleTime: 5000, + refetchOnWindowFocus: true, + }); +} + +const fetchSession = async () => { + const { data } = await axios.post("/lst/api/admin/users"); + + return data.data; +}; diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 4005320..ae66781 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -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 ( -
- - -
-
-
-
-
- ); + return ( +
+ + +
+
+
+
+
+ ); }; export const Route = createRootRouteWithContext()({ - component: RootLayout, + component: RootLayout, }); diff --git a/frontend/src/routes/_adminLayout/-components/ExpandedRow.tsx b/frontend/src/routes/_adminLayout/-components/ExpandedRow.tsx new file mode 100644 index 0000000..4e07a20 --- /dev/null +++ b/frontend/src/routes/_adminLayout/-components/ExpandedRow.tsx @@ -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>( + () => + 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 ( +
+
+ {modules.map((i) => { + return ( +
+ {i} + + + + + +
+ ); + })} +
+
+ ); +} diff --git a/frontend/src/routes/_adminLayout/admin/_users/users.tsx b/frontend/src/routes/_adminLayout/admin/_users/users.tsx index fca95b4..c3e9a5e 100644 --- a/frontend/src/routes/_adminLayout/admin/_users/users.tsx +++ b/frontend/src/routes/_adminLayout/admin/_users/users.tsx @@ -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
Hello "/_admin/admin/users "!
; + const { data, isLoading } = useQuery(getUsers()); + const [sorting, setSorting] = useState([]); + const columnHelper = createColumnHelper(); + + const columns = [ + columnHelper.accessor("username", { + cell: (i) => i.getValue(), + header: ({ column }) => { + return ( + + ); + }, + }), + + columnHelper.accessor("email", { + cell: (i) => i.getValue(), + header: ({ column }) => { + return ( + + ); + }, + }), + columnHelper.accessor("roles", { + header: () => Roles, + cell: ({ row }) => { + return row.getCanExpand() ? ( + + ) : ( + "No expanding" + ); + }, + }), + ]; + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + //renderSubComponent: ({ row }: { row: any }) => , + getRowCanExpand: () => true, + state: { + sorting, + }, + }); + + if (isLoading) { + return
Loading user data
; + } + + // render the roles card and make ts happy by not including it in the useReactTable hook + const renderSubComponent = ({ row }: { row: any }) => ( + + ); + + return ( +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + + {row.getIsExpanded() && ( + + + {renderSubComponent({ row })} + + + )} + + ))} + +
+
+
+ + +
+
+ ); }