mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-21 07:37:32 +00:00
Add user authentication and Active Directory management features. Includes a new admin UI and Swagger API documentation.
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/e9f0a10f-e323-455c-82c9-1da4a687f20e.jpg
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronsLeft,
|
||||
ChevronsRight,
|
||||
Search,
|
||||
SlidersHorizontal
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface DataTableColumn<T> {
|
||||
header: string;
|
||||
accessorKey: keyof T | ((row: T) => React.ReactNode);
|
||||
cell?: (row: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface DataTableProps<T> {
|
||||
data: T[];
|
||||
columns: DataTableColumn<T>[];
|
||||
isLoading?: boolean;
|
||||
onRowClick?: (row: T) => void;
|
||||
searchable?: boolean;
|
||||
filterable?: boolean;
|
||||
pagination?: {
|
||||
pageSize: number;
|
||||
pageIndex: number;
|
||||
pageCount: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onPageSizeChange: (size: number) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function DataTable<T>({
|
||||
data,
|
||||
columns,
|
||||
isLoading = false,
|
||||
onRowClick,
|
||||
searchable = false,
|
||||
filterable = false,
|
||||
pagination,
|
||||
}: DataTableProps<T>) {
|
||||
const [searchTerm, setSearchTerm] = React.useState("");
|
||||
|
||||
// Simple filtering based on searchTerm matching any string property
|
||||
const filteredData = React.useMemo(() => {
|
||||
if (!searchTerm) return data;
|
||||
|
||||
return data.filter(row => {
|
||||
return Object.entries(row as Record<string, any>).some(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
return value.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
}, [data, searchTerm]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{(searchable || filterable) && (
|
||||
<div className="flex flex-col sm:flex-row gap-2 mb-4">
|
||||
{searchable && (
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{filterable && (
|
||||
<Button variant="outline" className="sm:w-auto w-full">
|
||||
<SlidersHorizontal className="mr-2 h-4 w-4" />
|
||||
Filters
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-md border overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column, i) => (
|
||||
<TableHead key={i} className="bg-muted/50 font-medium">
|
||||
{column.header}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredData.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results found.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredData.map((row, i) => (
|
||||
<TableRow
|
||||
key={i}
|
||||
onClick={() => onRowClick?.(row)}
|
||||
className={onRowClick ? "cursor-pointer hover:bg-muted/50" : undefined}
|
||||
>
|
||||
{columns.map((column, j) => (
|
||||
<TableCell key={j}>
|
||||
{column.cell
|
||||
? column.cell(row)
|
||||
: typeof column.accessorKey === "function"
|
||||
? column.accessorKey(row)
|
||||
: (row[column.accessorKey] as React.ReactNode)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{pagination && (
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Rows per page:
|
||||
</p>
|
||||
<Select
|
||||
value={pagination.pageSize.toString()}
|
||||
onValueChange={(value) => pagination.onPageSizeChange(Number(value))}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[70px]">
|
||||
<SelectValue placeholder={pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{[10, 20, 30, 40, 50].map((size) => (
|
||||
<SelectItem key={size} value={size.toString()}>
|
||||
{size}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Page {pagination.pageIndex + 1} of {pagination.pageCount}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => pagination.onPageChange(0)}
|
||||
disabled={pagination.pageIndex === 0}
|
||||
>
|
||||
<ChevronsLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => pagination.onPageChange(pagination.pageIndex - 1)}
|
||||
disabled={pagination.pageIndex === 0}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => pagination.onPageChange(pagination.pageIndex + 1)}
|
||||
disabled={pagination.pageIndex === pagination.pageCount - 1}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => pagination.onPageChange(pagination.pageCount - 1)}
|
||||
disabled={pagination.pageIndex === pagination.pageCount - 1}
|
||||
>
|
||||
<ChevronsRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type StatusBadgeProps = {
|
||||
status: "connected" | "disconnected" | "success" | "error" | "warning" | "info";
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function StatusBadge({ status, children, className }: StatusBadgeProps) {
|
||||
const statusStyles = {
|
||||
connected: "bg-green-100 text-green-800 before:bg-green-500",
|
||||
disconnected: "bg-red-100 text-red-800 before:bg-red-500",
|
||||
success: "bg-green-100 text-green-800 before:bg-green-500",
|
||||
error: "bg-red-100 text-red-800 before:bg-red-500",
|
||||
warning: "bg-amber-100 text-amber-800 before:bg-amber-500",
|
||||
info: "bg-blue-100 text-blue-800 before:bg-blue-500",
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-full px-2 py-1 text-xs font-medium before:mr-1 before:h-2 before:w-2 before:rounded-full before:content-['']",
|
||||
statusStyles[status],
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user