All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 2s
✨ New Features - Advanced data table with TanStack Table v8 + Server Actions - Server-side pagination (10/25/50/100 rows per page) - Multi-column sorting with visual indicators - Column management (show/hide, resize) persisted to localStorage - URL state synchronization for shareable filtered views - Sticky header with compact/comfortable density modes 📦 Components Added - PolicyTableV2.tsx - Main table with TanStack integration - PolicyTableColumns.tsx - 7 column definitions with sorting - PolicyTablePagination.tsx - Pagination controls - PolicyTableToolbar.tsx - Density toggle + column visibility menu - ColumnVisibilityMenu.tsx - Show/hide columns dropdown 🔧 Hooks Added - usePolicyTable.ts - TanStack Table initialization - useURLState.ts - URL query param sync with nuqs - useTablePreferences.ts - localStorage persistence 🎨 Server Actions Updated - getPolicySettingsV2 - Pagination + sorting + filtering + Zod validation - exportPolicySettingsCSV - Server-side CSV generation (max 5000 rows) 📚 Documentation Added - Intune Migration Guide (1400+ lines) - Reverse engineering strategy - Intune Reference Version tracking - Tasks completed: 22/62 (Phase 1-3) ✅ Zero TypeScript compilation errors ✅ All MVP success criteria met (pagination, sorting, column management) ✅ Ready for Phase 4-7 (filtering, export, detail view, polish) Refs: specs/004-policy-explorer-v2/tasks.md
129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
/**
|
|
* PolicyTableV2
|
|
*
|
|
* Main data table component for Policy Explorer V2.
|
|
* Integrates TanStack Table with shadcn UI Table primitives.
|
|
*
|
|
* Features:
|
|
* - Server-side pagination via TanStack Table manual mode
|
|
* - Column sorting with visual indicators
|
|
* - Column resizing with drag handles
|
|
* - Sticky header (CSS position: sticky)
|
|
* - Row selection for CSV export
|
|
* - Density modes (compact/comfortable)
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import { flexRender } from '@tanstack/react-table';
|
|
import type { Table as TanStackTable } from '@tanstack/react-table';
|
|
import type { PolicySettingRow } from '@/lib/types/policy-table';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface PolicyTableV2Props {
|
|
table: TanStackTable<PolicySettingRow>;
|
|
density: 'compact' | 'comfortable';
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
export function PolicyTableV2({ table, density, isLoading = false }: PolicyTableV2Props) {
|
|
const rowHeight = density === 'compact' ? 'h-10' : 'h-14';
|
|
|
|
return (
|
|
<div className="relative rounded-md border">
|
|
<div className="overflow-auto max-h-[calc(100vh-300px)]">
|
|
<Table>
|
|
<TableHeader className="sticky top-0 z-10 bg-background">
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<TableRow key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => {
|
|
return (
|
|
<TableHead
|
|
key={header.id}
|
|
style={{
|
|
width: header.getSize() !== 150 ? header.getSize() : undefined,
|
|
}}
|
|
className="relative"
|
|
>
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(
|
|
header.column.columnDef.header,
|
|
header.getContext()
|
|
)}
|
|
|
|
{/* Column Resize Handle */}
|
|
{header.column.getCanResize() && (
|
|
<div
|
|
onMouseDown={header.getResizeHandler()}
|
|
onTouchStart={header.getResizeHandler()}
|
|
className={cn(
|
|
'absolute right-0 top-0 h-full w-1 cursor-col-resize select-none touch-none',
|
|
'hover:bg-primary',
|
|
header.column.getIsResizing() && 'bg-primary'
|
|
)}
|
|
/>
|
|
)}
|
|
</TableHead>
|
|
);
|
|
})}
|
|
</TableRow>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{isLoading ? (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={table.getAllColumns().length}
|
|
className="h-24 text-center"
|
|
>
|
|
Loading...
|
|
</TableCell>
|
|
</TableRow>
|
|
) : table.getRowModel().rows?.length ? (
|
|
table.getRowModel().rows.map((row) => (
|
|
<TableRow
|
|
key={row.id}
|
|
data-state={row.getIsSelected() && 'selected'}
|
|
className={cn(rowHeight)}
|
|
>
|
|
{row.getVisibleCells().map((cell) => (
|
|
<TableCell
|
|
key={cell.id}
|
|
style={{
|
|
width: cell.column.getSize() !== 150 ? cell.column.getSize() : undefined,
|
|
}}
|
|
>
|
|
{flexRender(
|
|
cell.column.columnDef.cell,
|
|
cell.getContext()
|
|
)}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={table.getAllColumns().length}
|
|
className="h-24 text-center"
|
|
>
|
|
No results found.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|