mosaicmap/app/admin/users/user-table.tsx
2025-08-14 21:34:16 +08:00

1012 lines
39 KiB
TypeScript

"use client"
import * as React from "react"
import { useQuery, gql } from '@apollo/client';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
closestCenter,
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
type DragEndEvent,
type UniqueIdentifier,
} from "@dnd-kit/core"
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import {
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronsLeft,
IconChevronsRight,
IconCircleCheckFilled,
IconDotsVertical,
IconGripVertical,
IconLayoutColumns,
IconLoader,
IconPlus,
IconTrendingUp,
} from "@tabler/icons-react"
import {
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
Row,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import { toast } from "sonner"
import { z } from "zod"
import { useIsMobile } from "@/hooks/use-mobile"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart"
import { Checkbox } from "@/components/ui/checkbox"
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
import CreateUserForm from "./create-user-form";
import { useUser } from "@/app/user-context";
export const schema = z.object({
user: z.object({
id: z.string(),
username: z.string(),
email: z.string(),
groups: z.array(z.string()),
createdAt: z.string(),
updatedAt: z.string(),
}),
groups: z.array(z.string()),
})
// Create a separate component for the drag handle
function DragHandle({ id }: { id: string }) {
const { attributes, listeners } = useSortable({
id,
})
return (
<Button
{...attributes}
{...listeners}
variant="ghost"
size="icon"
className="text-muted-foreground size-7 hover:bg-transparent"
>
<IconGripVertical className="text-muted-foreground size-3" />
<span className="sr-only">Drag to reorder</span>
</Button>
)
}
const columns: ColumnDef<z.infer<typeof schema>>[] = [
{
id: "drag",
header: () => null,
cell: ({ row }) => <DragHandle id={row.original.user.id} />,
},
{
id: "select",
header: ({ table }) => (
<div className="flex items-center justify-center">
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
</div>
),
cell: ({ row }) => (
<div className="flex items-center justify-center">
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
</div>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "name",
header: "Name",
cell: ({ row }) => {
return <TableCellViewer item={row.original} />
},
enableHiding: false,
},
{
accessorKey: "email",
header: "Email",
cell: ({ row }) => (
<div className="w-48">
<span className="text-sm text-muted-foreground">
{row.original.user.email}
</span>
</div>
),
},
{
accessorKey: "groups",
header: "Groups",
cell: ({ row }) => (
<div className="w-32">
<Badge variant="outline" className="text-muted-foreground px-1.5">
{row.original.groups.join(", ")}
</Badge>
</div>
),
},
{
accessorKey: "createdAt",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
className="px-0 hover:bg-transparent"
>
Created At
<IconChevronDown
className={`ml-2 h-4 w-4 transition-transform ${column.getIsSorted() === "asc" ? "rotate-180" : ""
}`}
/>
</Button>
)
},
cell: ({ row }) => {
const date = new Date(row.original.user.createdAt);
const now = new Date();
const diffInDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
let timeAgo = '';
if (diffInDays === 0) {
timeAgo = 'Today';
} else if (diffInDays === 1) {
timeAgo = 'Yesterday';
} else if (diffInDays < 7) {
timeAgo = `${diffInDays} days ago`;
} else if (diffInDays < 30) {
const weeks = Math.floor(diffInDays / 7);
timeAgo = `${weeks} week${weeks > 1 ? 's' : ''} ago`;
} else if (diffInDays < 365) {
const months = Math.floor(diffInDays / 30);
timeAgo = `${months} month${months > 1 ? 's' : ''} ago`;
} else {
const years = Math.floor(diffInDays / 365);
timeAgo = `${years} year${years > 1 ? 's' : ''} ago`;
}
return (
<div className="w-40">
<div className="flex flex-row gap-2">
<span className="text-sm font-medium">
{date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'short',
day: 'numeric'
})}
</span>
<Badge variant="secondary" className="text-xs w-fit">
{timeAgo}
</Badge>
</div>
</div>
);
},
enableSorting: true,
sortingFn: (rowA, rowB, columnId) => {
const dateA = new Date(rowA.original.user.createdAt);
const dateB = new Date(rowB.original.user.createdAt);
return dateA.getTime() - dateB.getTime();
},
},
{
id: "lastLogin",
accessorFn: (row) => row.user.updatedAt,
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
className="px-0 hover:bg-transparent"
>
Last Login
<IconChevronDown
className={`ml-2 h-4 w-4 transition-transform ${column.getIsSorted() === "asc" ? "rotate-180" : ""
}`}
/>
</Button>
)
},
cell: ({ row }) => {
const date = new Date(row.original.user.updatedAt);
const now = new Date();
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
let timeAgo = '';
if (diffInMinutes < 1) {
timeAgo = 'Just now';
} else if (diffInMinutes < 60) {
timeAgo = `${diffInMinutes} min${diffInMinutes > 1 ? 's' : ''} ago`;
} else if (diffInMinutes < 1440) { // 24 hours
const hours = Math.floor(diffInMinutes / 60);
timeAgo = `${hours} hour${hours > 1 ? 's' : ''} ago`;
} else {
const days = Math.floor(diffInMinutes / 1440);
timeAgo = `${days} day${days > 1 ? 's' : ''} ago`;
}
return (
<div className="w-40">
<div className="flex flex-row gap-2">
<span className="text-sm font-medium">
{date.toLocaleDateString('zh-CN', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</span>
<Badge variant="outline" className="text-xs w-fit">
{timeAgo}
</Badge>
</div>
</div>
);
},
enableSorting: true,
sortingFn: (rowA, rowB, columnId) => {
const dateA = new Date(rowA.original.user.updatedAt);
const dateB = new Date(rowB.original.user.updatedAt);
return dateA.getTime() - dateB.getTime();
},
},
{
id: "actions",
cell: () => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="data-[state=open]:bg-muted text-muted-foreground flex size-8"
size="icon"
>
<IconDotsVertical />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-42">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Reset Password</DropdownMenuItem>
<DropdownMenuItem>Ban User</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
]
function DraggableRow({ row }: { row: Row<z.infer<typeof schema>> }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({
id: row.original.user.id,
})
return (
<TableRow
data-state={row.getIsSelected() && "selected"}
data-dragging={isDragging}
ref={setNodeRef}
className="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
style={{
transform: CSS.Transform.toString(transform),
transition: transition,
}}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
}
const GET_USERS = gql`
query GetUsers($offset: Int, $limit: Int, $sort_by: String, $sort_order: String, $filter: String) {
userWithGroups(offset: $offset, limit: $limit, sortBy: $sort_by, sortOrder: $sort_order, filter: $filter) {
user{
id
username
email
createdAt
updatedAt
}
groups
}
}
`
const USERS_INFO = gql`
query UsersInfo($offset: Int, $limit: Int, $sort_by: String, $sort_order: String, $filter: String) {
usersInfo(offset: $offset, limit: $limit, sortBy: $sort_by, sortOrder: $sort_order, filter: $filter) {
totalUsers
totalActiveUsers
totalAdminUsers
totalUserUsers
users {
user {
id
username
email
createdAt
updatedAt
}
groups
}
}
}
`
export function UserTable() {
const { data, loading, error, refetch } = useQuery(USERS_INFO)
const [localData, setLocalData] = React.useState<any[]>([])
React.useEffect(() => {
if (data && Array.isArray(data.usersInfo.users)) {
setLocalData(data.usersInfo.users)
}
}, [data])
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event
if (active && over && active.id !== over.id) {
setLocalData((currentData) => {
const oldIndex = currentData.findIndex((item) => item.id === active.id)
const newIndex = currentData.findIndex((item) => item.id === over.id)
if (oldIndex === -1 || newIndex === -1) return currentData
// 只做本地排序,不保存到数据库
return arrayMove(currentData, oldIndex, newIndex)
})
}
}
return (
<>
{error && <div className="text-red-500">{error.message}</div>}
<UserTabs
initialData={localData}
initialLoading={loading}
info={data?.usersInfo}
handleDragEnd={handleDragEnd}
refetch={refetch}
/>
</>
)
}
function UserTabs({ initialData, initialLoading, info, handleDragEnd, refetch }: { initialData: any[], initialLoading: boolean, info: any, handleDragEnd: (event: DragEndEvent) => void, refetch: any }) {
return (
<Dialog>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<CreateUserForm />
</DialogContent>
<Tabs
defaultValue="all_users"
className="w-full flex-col justify-start gap-6"
>
<div className="flex items-center justify-between px-4 lg:px-6">
<Label htmlFor="view-selector" className="sr-only">
View
</Label>
<Select defaultValue="outline">
<SelectTrigger
className="flex w-fit @4xl/main:hidden"
size="sm"
id="view-selector"
>
<SelectValue placeholder="Select a view" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all_users">All Users</SelectItem>
<SelectItem value="active_users">Active Users</SelectItem>
<SelectItem value="admin_users">Administrators</SelectItem>
<SelectItem value="inactive_users">Inactive Users</SelectItem>
</SelectContent>
</Select>
<TabsList className="**:data-[slot=badge]:bg-muted-foreground/30 hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:px-1 @4xl/main:flex">
<TabsTrigger value="all_users">All Users</TabsTrigger>
<TabsTrigger value="active_users">
Active Users <Badge variant="secondary">{info?.totalActiveUsers}</Badge>
</TabsTrigger>
<TabsTrigger value="admin_users">
Administrators <Badge variant="secondary">{info?.totalAdminUsers}</Badge>
</TabsTrigger>
<TabsTrigger value="inactive_users">Inactive Users</TabsTrigger>
</TabsList>
<div className="flex items-center gap-2">
<DialogTrigger asChild>
<Button variant="outline" size="sm">
<IconPlus />
<span className="hidden lg:inline">Add User</span>
</Button>
</DialogTrigger>
</div>
</div>
<TabsContent value="all_users" className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6">
<UserDataTable
data={initialData}
isLoading={initialLoading}
handleDragEnd={handleDragEnd}
useInitialData={true}
enableServerSideSort={true}
refetchFn={refetch}
/>
</TabsContent>
<TabsContent value="active_users" className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6">
<UserDataTable
filter="Users"
handleDragEnd={handleDragEnd}
useInitialData={false}
/>
</TabsContent>
<TabsContent value="admin_users" className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6">
<UserDataTable
filter="Admin"
handleDragEnd={handleDragEnd}
useInitialData={false}
/>
</TabsContent>
<TabsContent value="inactive_users" className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6">
<UserDataTable
filter="Users"
handleDragEnd={handleDragEnd}
useInitialData={false}
/>
</TabsContent>
</Tabs>
</Dialog>
)
}
// 可重用的数据表格组件
function UserDataTable({
data: propData,
isLoading: propIsLoading = false,
handleDragEnd,
filter,
useInitialData = false,
enableServerSideSort = false,
refetchFn
}: {
data?: any[]
isLoading?: boolean
handleDragEnd: (event: DragEndEvent) => void
filter?: string
useInitialData?: boolean
enableServerSideSort?: boolean
refetchFn?: any
}) {
const [localData, setLocalData] = React.useState<any[]>([])
const [rowSelection, setRowSelection] = React.useState({})
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const [sorting, setSorting] = React.useState<SortingState>([])
const [pagination, setPagination] = React.useState({
pageIndex: 0,
pageSize: 10,
})
// 为非初始数据的tab查询数据
const { data: queryData, loading: queryLoading, refetch } = useQuery(GET_USERS, {
variables: {
offset: pagination.pageIndex * pagination.pageSize,
limit: pagination.pageSize,
sortBy: sorting[0]?.id || "createdAt",
sortOrder: sorting[0]?.desc ? "DESC" : "ASC",
filter: filter
},
skip: useInitialData,
fetchPolicy: 'cache-and-network'
})
const data = useInitialData ? propData : queryData?.userWithGroups
const isLoading = useInitialData ? propIsLoading : queryLoading
// 同步数据到本地状态
React.useEffect(() => {
debugger
if (data && Array.isArray(data)) {
setLocalData(data)
}
}, [data])
// 当筛选条件变化时重置分页
React.useEffect(() => {
setPagination({ pageIndex: 0, pageSize: 10 })
}, [filter])
// 当排序或分页变化时,重新查询数据
React.useEffect(() => {
const refetchFunc = refetchFn || refetch
if ((!useInitialData || enableServerSideSort) && refetchFunc) {
refetchFunc({
offset: pagination.pageIndex * pagination.pageSize,
limit: pagination.pageSize,
sortBy: sorting[0]?.id || "createdAt",
sortOrder: sorting[0]?.desc ? "DESC" : "ASC",
filter: filter
})
}
}, [sorting, pagination, filter, useInitialData, enableServerSideSort, refetch, refetchFn])
function handleLocalDragEnd(event: DragEndEvent) {
const { active, over } = event
if (active && over && active.id !== over.id) {
setLocalData((currentData) => {
const oldIndex = currentData.findIndex((item) => item.id === active.id)
const newIndex = currentData.findIndex((item) => item.id === over.id)
if (oldIndex === -1 || newIndex === -1) return currentData
// 只做本地排序,不保存到数据库
return arrayMove(currentData, oldIndex, newIndex)
})
}
// 也调用父组件的handleDragEnd
handleDragEnd(event)
}
const sortableId = React.useId()
const sensors = useSensors(
useSensor(MouseSensor, {}),
useSensor(TouchSensor, {}),
useSensor(KeyboardSensor, {})
)
const table = useReactTable({
data: localData || [],
columns,
state: {
sorting,
columnVisibility,
rowSelection,
columnFilters,
pagination,
},
getRowId: (row) => row.user.id.toString(),
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
manualPagination: true,
})
return (
<>
{/* <div className="flex items-center gap-2 mb-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<IconLayoutColumns />
<span className="hidden lg:inline">Customize Columns</span>
<span className="lg:hidden">Columns</span>
<IconChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" &&
column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
</div> */}
<div className="overflow-hidden rounded-lg border">
<DndContext
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
onDragEnd={handleLocalDragEnd}
sensors={sensors}
id={sortableId}
>
<Table>
<TableHeader className="bg-muted sticky top-0 z-10">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody className="**:data-[slot=table-cell]:first:w-8">
{isLoading ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center">
Loading...
{/* <IconLoader className="size-4 animate-spin" /> */}
</TableCell>
</TableRow>
) : table.getRowModel().rows?.length ? (
<SortableContext
items={localData?.map((user: any) => user.id) || []}
strategy={verticalListSortingStrategy}
>
{table.getRowModel().rows.map((row) => (
<DraggableRow key={row.id} row={row} />
))}
</SortableContext>
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</DndContext>
</div>
<div className="flex items-center justify-between px-4">
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex w-full items-center gap-8 lg:w-fit">
<div className="hidden items-center gap-2 lg:flex">
<Label htmlFor="rows-per-page" className="text-sm font-medium">
Rows per page
</Label>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
table.setPageSize(Number(value))
}}
>
<SelectTrigger size="sm" className="w-20" id="rows-per-page">
<SelectValue
placeholder={table.getState().pagination.pageSize}
/>
</SelectTrigger>
<SelectContent side="top">
{[10, 20, 30, 40, 50].map((pageSize) => (
<SelectItem key={pageSize} value={`${pageSize}`}>
{pageSize}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-fit items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
<div className="ml-auto flex items-center gap-2 lg:ml-0">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<IconChevronsLeft />
</Button>
<Button
variant="outline"
className="size-8"
size="icon"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<IconChevronLeft />
</Button>
<Button
variant="outline"
className="size-8"
size="icon"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<IconChevronRight />
</Button>
<Button
variant="outline"
className="hidden size-8 lg:flex"
size="icon"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<IconChevronsRight />
</Button>
</div>
</div>
</div>
</>
)
}
const chartData = [
{ month: "January", desktop: 186, mobile: 80 },
{ month: "February", desktop: 305, mobile: 200 },
{ month: "March", desktop: 237, mobile: 120 },
{ month: "April", desktop: 73, mobile: 190 },
{ month: "May", desktop: 209, mobile: 130 },
{ month: "June", desktop: 214, mobile: 140 },
]
const chartConfig = {
desktop: {
label: "Desktop",
color: "var(--primary)",
},
mobile: {
label: "Mobile",
color: "var(--primary)",
},
} satisfies ChartConfig
function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
const isMobile = useIsMobile()
const { user } = useUser()
return (
<Drawer direction={isMobile ? "bottom" : "right"}>
<DrawerTrigger asChild>
<div className="flex items-center gap-2">
<Button variant="link" className="text-foreground w-fit px-0 text-left">
{item.user.username}
</Button>
{
item.user.id === user?.id ? (
<Badge variant="secondary" className="text-[10px] w-fit">
Me
</Badge>
) : <></>
}
</div>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="gap-1">
<DrawerTitle>{item.user.username}</DrawerTitle>
<DrawerDescription>
User profile and activity information
</DrawerDescription>
</DrawerHeader>
<div className="flex flex-col gap-4 overflow-y-auto px-4 text-sm">
{!isMobile && (
<>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={chartData}
margin={{
left: 0,
right: 10,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="month"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
hide
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="dot" />}
/>
<Area
dataKey="mobile"
type="natural"
fill="var(--color-mobile)"
fillOpacity={0.6}
stroke="var(--color-mobile)"
stackId="a"
/>
<Area
dataKey="desktop"
type="natural"
fill="var(--color-desktop)"
fillOpacity={0.4}
stroke="var(--color-desktop)"
stackId="a"
/>
</AreaChart>
</ChartContainer>
<Separator />
<div className="grid gap-2">
<div className="flex gap-2 leading-none font-medium">
Login activity up by 5.2% this month{" "}
<IconTrendingUp className="size-4" />
</div>
<div className="text-muted-foreground">
User activity and login statistics for the last 6 months. This shows
the user's engagement patterns and system usage over time.
</div>
</div>
<Separator />
</>
)}
<form className="flex flex-col gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="name">Name</Label>
<Input id="name" defaultValue={item.user.username} />
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="email">Email</Label>
<Input id="email" defaultValue={item.user.email} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="role">Roles</Label>
<Select defaultValue={item.user.username}>
<SelectTrigger id="role" className="w-full">
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Administrator">Administrator</SelectItem>
<SelectItem value="Manager">Manager</SelectItem>
<SelectItem value="Developer">Developer</SelectItem>
<SelectItem value="Designer">Designer</SelectItem>
<SelectItem value="Analyst">Analyst</SelectItem>
<SelectItem value="Support">Support</SelectItem>
<SelectItem value="Guest">Guest</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</form>
</div>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose asChild>
<Button variant="outline">Done</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}