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
171 lines
5.6 KiB
TypeScript
171 lines
5.6 KiB
TypeScript
/**
|
|
* PolicyExplorerV2Client
|
|
*
|
|
* Client component wrapper for Policy Explorer V2.
|
|
* Manages state, fetches data via Server Actions, and orchestrates all subcomponents.
|
|
*
|
|
* This component:
|
|
* - Uses useURLState for pagination, sorting, filtering
|
|
* - Uses useTablePreferences for localStorage persistence
|
|
* - Uses usePolicyTable for TanStack Table integration
|
|
* - Fetches data via getPolicySettingsV2 Server Action
|
|
* - Renders PolicyTableToolbar, PolicyTableV2, PolicyTablePagination
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useEffect, useState, useCallback } from 'react';
|
|
import { useURLState } from '@/lib/hooks/useURLState';
|
|
import { useTablePreferences } from '@/lib/hooks/useTablePreferences';
|
|
import { usePolicyTable } from '@/lib/hooks/usePolicyTable';
|
|
import { PolicyTableV2 } from '@/components/policy-explorer/PolicyTableV2';
|
|
import { PolicyTableToolbar } from '@/components/policy-explorer/PolicyTableToolbar';
|
|
import { PolicyTablePagination } from '@/components/policy-explorer/PolicyTablePagination';
|
|
import { policyTableColumns } from '@/components/policy-explorer/PolicyTableColumns';
|
|
import { getPolicySettingsV2 } from '@/lib/actions/policySettings';
|
|
import type { PolicySettingRow, PaginationMeta } from '@/lib/types/policy-table';
|
|
|
|
export function PolicyExplorerV2Client() {
|
|
const [data, setData] = useState<PolicySettingRow[]>([]);
|
|
const [meta, setMeta] = useState<PaginationMeta | undefined>();
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// URL state management
|
|
const urlState = useURLState();
|
|
|
|
// localStorage preferences
|
|
const {
|
|
preferences,
|
|
isLoaded: preferencesLoaded,
|
|
updateColumnVisibility,
|
|
updateColumnSizing,
|
|
updateDensity,
|
|
updateDefaultPageSize,
|
|
} = useTablePreferences();
|
|
|
|
// Fetch data via Server Action
|
|
const fetchData = useCallback(async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const result = await getPolicySettingsV2({
|
|
page: urlState.page,
|
|
pageSize: urlState.pageSize as 10 | 25 | 50 | 100,
|
|
sortBy: urlState.sortBy as 'settingName' | 'policyName' | 'policyType' | 'lastSyncedAt' | undefined,
|
|
sortDir: urlState.sortDir,
|
|
policyTypes: urlState.policyTypes.length > 0 ? urlState.policyTypes : undefined,
|
|
searchQuery: urlState.searchQuery || undefined,
|
|
});
|
|
|
|
if (result.success) {
|
|
setData(result.data || []);
|
|
setMeta(result.meta);
|
|
} else {
|
|
setError(result.error || 'Failed to fetch data');
|
|
}
|
|
} catch (err) {
|
|
console.error('Fetch error:', err);
|
|
setError('An unexpected error occurred');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [urlState.page, urlState.pageSize, urlState.sortBy, urlState.sortDir, urlState.policyTypes, urlState.searchQuery]);
|
|
|
|
// Fetch data when URL state changes
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
// TanStack Table integration
|
|
const { table, selectedRows, selectedCount, totalCount, hasSelection } = usePolicyTable({
|
|
data,
|
|
columns: policyTableColumns,
|
|
pagination: {
|
|
pageIndex: urlState.page,
|
|
pageSize: urlState.pageSize as 10 | 25 | 50 | 100,
|
|
},
|
|
onPaginationChange: (updater) => {
|
|
const newPagination = typeof updater === 'function'
|
|
? updater({ pageIndex: urlState.page, pageSize: urlState.pageSize as 10 | 25 | 50 | 100 })
|
|
: updater;
|
|
|
|
urlState.updatePage(newPagination.pageIndex);
|
|
if (newPagination.pageSize !== urlState.pageSize) {
|
|
urlState.updatePageSize(newPagination.pageSize);
|
|
}
|
|
},
|
|
sorting: urlState.sortBy
|
|
? [{ id: urlState.sortBy, desc: urlState.sortDir === 'desc' }]
|
|
: [],
|
|
onSortingChange: (updater) => {
|
|
const newSorting = typeof updater === 'function'
|
|
? updater(urlState.sortBy ? [{ id: urlState.sortBy, desc: urlState.sortDir === 'desc' }] : [])
|
|
: updater;
|
|
|
|
if (newSorting.length > 0) {
|
|
urlState.updateSorting(newSorting[0].id, newSorting[0].desc ? 'desc' : 'asc');
|
|
}
|
|
},
|
|
columnVisibility: preferencesLoaded ? preferences.columnVisibility : {},
|
|
onColumnVisibilityChange: (updater) => {
|
|
const newVisibility = typeof updater === 'function'
|
|
? updater(preferences.columnVisibility)
|
|
: updater;
|
|
updateColumnVisibility(newVisibility);
|
|
},
|
|
columnSizing: preferencesLoaded ? preferences.columnSizing : {},
|
|
onColumnSizingChange: (updater) => {
|
|
const newSizing = typeof updater === 'function'
|
|
? updater(preferences.columnSizing)
|
|
: updater;
|
|
updateColumnSizing(newSizing);
|
|
},
|
|
meta,
|
|
enableRowSelection: true,
|
|
});
|
|
|
|
// Handle density change
|
|
const handleDensityChange = useCallback((density: 'compact' | 'comfortable') => {
|
|
updateDensity(density);
|
|
}, [updateDensity]);
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="rounded-md border border-destructive p-4 text-center">
|
|
<p className="text-destructive font-semibold">Error loading policy settings</p>
|
|
<p className="text-sm text-muted-foreground mt-2">{error}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Toolbar */}
|
|
<PolicyTableToolbar
|
|
table={table}
|
|
density={preferences.density}
|
|
onDensityChange={handleDensityChange}
|
|
/>
|
|
|
|
{/* Table */}
|
|
<PolicyTableV2
|
|
table={table}
|
|
density={preferences.density}
|
|
isLoading={isLoading}
|
|
/>
|
|
|
|
{/* Pagination */}
|
|
{meta && (
|
|
<PolicyTablePagination
|
|
table={table}
|
|
totalCount={meta.totalCount}
|
|
pageCount={meta.pageCount}
|
|
currentPage={meta.currentPage}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|