All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 2s
✨ 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
180 lines
6.0 KiB
TypeScript
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>
|
|
);
|
|
}
|