feat: initialize Infra Manager with React, Tailwind CSS, and Inertia.js
- Added main application entry point (app.jsx) and bootstrap file for React setup. - Created Wiki index page (Index.jsx) for documentation and file management. - Implemented utility functions for formatting and status handling in utils.js. - Set up Blade template (app.blade.php) for Inertia.js integration. - Defined API routes for metrics and terminal access in api.php. - Established console commands for scheduled tasks in console.php. - Created web routes for various application features including authentication, hypervisors, containers, cloud accounts, security, backups, and wiki management in web.php. - Developed setup scripts for Windows (setup.ps1) and Linux (setup.sh) environments. - Configured Tailwind CSS for styling with custom themes and animations in tailwind.config.js. - Set up Vite for asset bundling and development server in vite.config.js.
This commit is contained in:
137
resources/js/Components/ui/DataTable.jsx
Normal file
137
resources/js/Components/ui/DataTable.jsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { ChevronUp, ChevronDown, Search, ChevronsLeft, ChevronsRight, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function DataTable({ columns, data, onRowClick, emptyMessage = 'No data found', searchable = true, className }) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [sortCol, setSortCol] = useState(null);
|
||||
const [sortDir, setSortDir] = useState('asc');
|
||||
const [page, setPage] = useState(1);
|
||||
const perPage = 15;
|
||||
|
||||
const filtered = search
|
||||
? data.filter(row => columns.some(col => {
|
||||
const val = col.accessor ? row[col.accessor] : '';
|
||||
return String(val ?? '').toLowerCase().includes(search.toLowerCase());
|
||||
}))
|
||||
: data;
|
||||
|
||||
const sorted = sortCol
|
||||
? [...filtered].sort((a, b) => {
|
||||
const va = a[sortCol] ?? '';
|
||||
const vb = b[sortCol] ?? '';
|
||||
const cmp = String(va).localeCompare(String(vb), undefined, { numeric: true });
|
||||
return sortDir === 'asc' ? cmp : -cmp;
|
||||
})
|
||||
: filtered;
|
||||
|
||||
const totalPages = Math.ceil(sorted.length / perPage);
|
||||
const paginated = sorted.slice((page - 1) * perPage, page * perPage);
|
||||
|
||||
const handleSort = (accessor) => {
|
||||
if (sortCol === accessor) {
|
||||
setSortDir(d => d === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortCol(accessor);
|
||||
setSortDir('asc');
|
||||
}
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-3', className)}>
|
||||
{searchable && (
|
||||
<div className="relative w-72">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
value={search}
|
||||
onChange={e => { setSearch(e.target.value); setPage(1); }}
|
||||
className="w-full pl-9 pr-4 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-cyan-500/50 focus:bg-white/[0.07] transition-all"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border border-white/10">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10 bg-white/[0.02]">
|
||||
{columns.map(col => (
|
||||
<th key={col.key ?? col.accessor}
|
||||
className={cn(
|
||||
'text-left px-4 py-3 text-xs font-medium text-white/50 uppercase tracking-wider whitespace-nowrap',
|
||||
col.accessor && 'cursor-pointer hover:text-white/70 select-none'
|
||||
)}
|
||||
onClick={() => col.accessor && handleSort(col.accessor)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
{col.header}
|
||||
{col.accessor && sortCol === col.accessor && (
|
||||
sortDir === 'asc'
|
||||
? <ChevronUp className="w-3 h-3 text-cyan-400" />
|
||||
: <ChevronDown className="w-3 h-3 text-cyan-400" />
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<AnimatePresence mode="wait">
|
||||
{paginated.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="text-center py-12 text-white/30">
|
||||
{emptyMessage}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
paginated.map((row, i) => (
|
||||
<motion.tr
|
||||
key={row.id ?? i}
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: i * 0.02 }}
|
||||
onClick={() => onRowClick?.(row)}
|
||||
className={cn(
|
||||
'border-b border-white/[0.05] last:border-0 transition-colors',
|
||||
onRowClick && 'cursor-pointer hover:bg-white/[0.03]'
|
||||
)}
|
||||
>
|
||||
{columns.map(col => (
|
||||
<td key={col.key ?? col.accessor} className="px-4 py-3 text-white/80">
|
||||
{col.render ? col.render(row) : row[col.accessor]}
|
||||
</td>
|
||||
))}
|
||||
</motion.tr>
|
||||
))
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between text-xs text-white/40">
|
||||
<span>{filtered.length} results</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={() => setPage(1)} disabled={page === 1} className="p-1 hover:text-white disabled:opacity-30">
|
||||
<ChevronsLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => setPage(p => p - 1)} disabled={page === 1} className="p-1 hover:text-white disabled:opacity-30">
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="px-3 text-white">Page {page} of {totalPages}</span>
|
||||
<button onClick={() => setPage(p => p + 1)} disabled={page === totalPages} className="p-1 hover:text-white disabled:opacity-30">
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => setPage(totalPages)} disabled={page === totalPages} className="p-1 hover:text-white disabled:opacity-30">
|
||||
<ChevronsRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user