tenantpilot/app/(app)/search/PolicyExplorerV2Client.tsx
Ahmed Darrazi 41e80b6c0c
All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 2s
feat(policy-explorer-v2): implement MVP Phase 1-3
 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
2025-12-10 00:18:05 +01:00

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>
);
}