diff --git a/my-app/app/admin/groups/groups-table-columns.tsx b/my-app/app/admin/groups/groups-table-columns.tsx index c731e0f..4f53a45 100644 --- a/my-app/app/admin/groups/groups-table-columns.tsx +++ b/my-app/app/admin/groups/groups-table-columns.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { MoreVertical, Trash2, Pencil } from "lucide-react"; +import { MoreVertical, Trash2, Edit } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { SortingIcon } from "@/components/table-components"; @@ -18,13 +18,19 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { Group } from "@/lib/types"; import { SortingState } from "@tanstack/react-table"; +import { formatRelativeTime, formatDateTime } from "@/lib/utils"; interface GroupsTableCoreProps { groups: Group[]; searchTerm: string; - onGroupAction: (groupName: string, action: "delete" | "edit") => void; + onGroupAction: (groupName: string, action: "delete" | "rename") => void; selectedGroups: string[]; onSelectGroup: (groupName: string, checked: boolean) => void; onSelectAll: (checked: boolean) => void; @@ -87,7 +93,7 @@ export function GroupsTableCore({ disabled={allSelectableGroups.length === 0} /> - +
- +
- -
- Comment + +
+ +
- + + + + + + {/* Delete Confirmation Dialog */} diff --git a/my-app/app/admin/users/edit-groups-dialog.tsx b/my-app/app/admin/users/edit-groups-dialog.tsx index a5f10b8..e0d806a 100644 --- a/my-app/app/admin/users/edit-groups-dialog.tsx +++ b/my-app/app/admin/users/edit-groups-dialog.tsx @@ -62,10 +62,10 @@ export function EditGroupsDialog({ // Set initial selected groups when user changes useEffect(() => { if (user) { - // Filter out empty strings from groups - const validGroups = user.groups.filter((g) => g && g.trim() !== ""); - setSelectedGroups(validGroups); - setInitialGroups(validGroups); + // Extract group names from Group objects + const groupNames = user.groups.map((g) => g.name); + setSelectedGroups(groupNames); + setInitialGroups(groupNames); } }, [user]); diff --git a/my-app/app/admin/users/header-stats.tsx b/my-app/app/admin/users/header-stats.tsx index e32f952..631401d 100644 --- a/my-app/app/admin/users/header-stats.tsx +++ b/my-app/app/admin/users/header-stats.tsx @@ -7,21 +7,27 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { GetUsersResponse } from "@/lib/types"; -import { User } from "lucide-react"; +import { User, UserX, Shield, Pencil } from "lucide-react"; import { Button } from "@/components/ui/button"; import { CreateUserDialog } from "@/components/shared/create-user-dialog"; +interface UserStats { + count: number; + disabled_count: number; + admin_count: number; + creator_count: number; +} + interface HeaderStatsProps { - usersData: GetUsersResponse; + stats: UserStats; onUserCreated?: () => void; } -export function HeaderStats({ usersData, onUserCreated }: HeaderStatsProps) { +export function HeaderStats({ stats, onUserCreated }: HeaderStatsProps) { return ( <>
@@ -30,7 +36,43 @@ export function HeaderStats({ usersData, onUserCreated }: HeaderStatsProps) { - {usersData.count} + {stats.count} + + + + + + + Disabled Users + + + + + {stats.disabled_count} + + + + + + + Admin Users + + + + + {stats.admin_count} + + + + + + + Creator Users + + + + + {stats.creator_count} diff --git a/my-app/app/admin/users/use-user-filters.ts b/my-app/app/admin/users/use-user-filters.ts index 1d5a51a..c533451 100644 --- a/my-app/app/admin/users/use-user-filters.ts +++ b/my-app/app/admin/users/use-user-filters.ts @@ -23,7 +23,7 @@ export function useUserFilters({ users, searchTerm }: UseUserFiltersProps) { // Search in groups if ( user.groups.some((group) => - group.toLowerCase().includes(lowercaseSearch), + group.name.toLowerCase().includes(lowercaseSearch), ) ) { return true; diff --git a/my-app/app/admin/users/users-table-columns.tsx b/my-app/app/admin/users/users-table-columns.tsx index 9892b7d..890b097 100644 --- a/my-app/app/admin/users/users-table-columns.tsx +++ b/my-app/app/admin/users/users-table-columns.tsx @@ -18,9 +18,15 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { User } from "@/lib/types"; import { Badge } from "@/components/ui/badge"; import { SortingState } from "@tanstack/react-table"; +import { formatRelativeTime, formatDateTime } from "@/lib/utils"; interface UsersTableCoreProps { users: User[]; @@ -122,6 +128,19 @@ export function UsersTableCore({ Groups + +
+ + +
+
@@ -147,7 +166,7 @@ export function UsersTableCore({ {users.length === 0 && ( {searchTerm @@ -172,24 +191,37 @@ export function UsersTableCore({
- {user.groups && - user.groups.filter((g) => g && g.trim() !== "").length > 0 ? ( - user.groups - .filter((g) => g && g.trim() !== "") - .map((group) => ( - - {group} - - )) + {user.groups && user.groups.length > 0 ? ( + user.groups.map((group) => ( + + {group.name} + + )) ) : ( N/A )}
+ + {user.created_at ? ( + + + + {formatRelativeTime(user.created_at)} + + + +

{formatDateTime(user.created_at)}

+
+
+ ) : ( + N/A + )} +
diff --git a/my-app/app/admin/users/users-table.tsx b/my-app/app/admin/users/users-table.tsx index 094aacc..c0724f5 100644 --- a/my-app/app/admin/users/users-table.tsx +++ b/my-app/app/admin/users/users-table.tsx @@ -28,10 +28,7 @@ interface UsersTableProps { export function UsersTable({ onUserAction, onRefresh }: UsersTableProps) { const [searchTerm, setSearchTerm] = React.useState(""); const [isRefreshing, setIsRefreshing] = React.useState(false); - const [usersData, setUsersData] = React.useState({ - users: [], - count: 0, - }); + const [users, setUsers] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); @@ -46,7 +43,7 @@ export function UsersTable({ onUserAction, onRefresh }: UsersTableProps) { // Sorting state const [sorting, setSorting] = React.useState([ - { id: "name", desc: false }, // Default sort by name + { id: "created_at", desc: false }, // Default sort by created_at (recent to old) ]); // Dialog states @@ -56,9 +53,19 @@ export function UsersTable({ onUserAction, onRefresh }: UsersTableProps) { const [deleteConfirmDialogOpen, setDeleteConfirmDialogOpen] = React.useState(false); + // Compute stats from users data + const stats = React.useMemo(() => { + return { + count: users.length, + disabled_count: users.filter((u) => !u.enabled).length, + admin_count: users.filter((u) => u.is_admin).length, + creator_count: users.filter((u) => u.is_creator).length, + }; + }, [users]); + // Apply filters const filteredUsers = useUserFilters({ - users: usersData.users, + users, searchTerm, }); @@ -79,6 +86,11 @@ export function UsersTable({ onUserAction, onRefresh }: UsersTableProps) { } else if (sort.id === "groups") { aValue = a.groups.length; bValue = b.groups.length; + } else if (sort.id === "created_at") { + // Sort by date (most recent first by default) + const aDate = a.created_at ? new Date(a.created_at).getTime() : 0; + const bDate = b.created_at ? new Date(b.created_at).getTime() : 0; + return sort.desc ? aDate - bDate : bDate - aDate; } else { return 0; } @@ -98,8 +110,8 @@ export function UsersTable({ onUserAction, onRefresh }: UsersTableProps) { // Get selected user objects const selectedUserObjects = React.useMemo(() => { - return usersData.users.filter((user) => selectedUsers.has(user.name)); - }, [usersData.users, selectedUsers]); + return users.filter((user) => selectedUsers.has(user.name)); + }, [users, selectedUsers]); // Bulk operation handlers const handleBulkAddToGroup = () => { @@ -206,7 +218,7 @@ export function UsersTable({ onUserAction, onRefresh }: UsersTableProps) { setLoading(true); setError(null); const fetchedUsersData = await getAllUsers(); - setUsersData(fetchedUsersData); + setUsers(fetchedUsersData.users); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to fetch users"; @@ -272,7 +284,7 @@ export function UsersTable({ onUserAction, onRefresh }: UsersTableProps) { return (
{/* Header Stats */} - + {/* Users Table */}
diff --git a/my-app/app/page.tsx b/my-app/app/page.tsx index b56a6f0..dae5e97 100644 --- a/my-app/app/page.tsx +++ b/my-app/app/page.tsx @@ -147,8 +147,8 @@ export default function Page() { - {currentUserData.groups.map((group: string) => ( -

{group}

+ {currentUserData.groups.map((group) => ( +

{group.name}

))}
diff --git a/my-app/lib/api.ts b/my-app/lib/api.ts index 541f721..79a8381 100644 --- a/my-app/lib/api.ts +++ b/my-app/lib/api.ts @@ -278,13 +278,11 @@ export async function getAllUsers(): Promise { }), ); - // Normalize groups to ensure it's always an array and filter out empty strings + // Ensure groups is always an array if (data.users) { data.users = data.users.map((user) => ({ ...user, - groups: Array.isArray(user.groups) - ? user.groups.filter((g) => g && g.trim() !== "") - : [], + groups: Array.isArray(user.groups) ? user.groups : [], })); } diff --git a/my-app/lib/types.ts b/my-app/lib/types.ts index db26a9f..27d81b1 100644 --- a/my-app/lib/types.ts +++ b/my-app/lib/types.ts @@ -33,15 +33,20 @@ export interface DeployedPodResponse { pods: DeployedPod[]; } -export interface User { +export interface Group { name: string; - groups: string[]; + can_modify: boolean; + created_at?: string; + user_count?: number; } -export interface Group { +export interface User { name: string; - user_count?: number; - comment?: string; + created_at: string; + enabled: boolean; + is_admin: boolean; + is_creator: boolean; + groups: Group[]; } export interface UserLogin { @@ -93,7 +98,10 @@ export interface VirtualMachinesResponse { export interface GetUsersResponse { users: User[]; - count: number; + count?: number; + disabled_count?: number; + admin_count?: number; + creator_count?: number; } export interface Resources {