tenantpilot/app/(app)/search/PolicyExplorerV2Client.tsx
Ahmed Darrazi 4288bc7884
All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 2s
feat(policy-explorer-v2): implement Phase 5 - Bulk CSV Export
 New Features (Tasks T029-T037)
- Client-side CSV export for selected rows
- Server-side CSV export for all filtered results (max 5000)
- RFC 4180 compliant CSV formatting with proper escaping
- UTF-8 BOM for Excel compatibility
- ExportButton dropdown with two export modes
- Warning UI when results exceed 5000 rows
- Loading state with spinner during server export

📦 New Files
- lib/utils/csv-export.ts - CSV generation utilities
- components/policy-explorer/ExportButton.tsx - Export dropdown

🔧 Updates
- PolicyTableToolbar now includes ExportButton
- PolicyExplorerV2Client passes export props
- Filename generation with timestamp and row count

 Zero TypeScript compilation errors
 All Phase 5 tasks complete (T029-T037)
 Ready for Phase 6 (Enhanced Detail View)

Refs: specs/004-policy-explorer-v2/tasks.md Phase 5
2025-12-10 00:30:33 +01:00

180 lines
6.0 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}
selectedPolicyTypes={urlState.policyTypes}
onSelectedPolicyTypesChange={urlState.updatePolicyTypes}
searchQuery={urlState.searchQuery}
onSearchQueryChange={urlState.updateSearchQuery}
selectedRows={selectedRows}
selectedCount={selectedCount}
totalCount={totalCount}
sortBy={urlState.sortBy}
sortDir={urlState.sortDir}
/>
{/* Table */}
<PolicyTableV2
table={table}
density={preferences.density}
isLoading={isLoading}
/>
{/* Pagination */}
{meta && (
<PolicyTablePagination
table={table}
totalCount={meta.totalCount}
pageCount={meta.pageCount}
currentPage={meta.currentPage}
/>
)}
</div>
);
}