tenantpilot/components/policy-explorer/ExportButton.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

166 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ExportButton Component
*
* CSV export dropdown with two options:
* 1. Export Selected Rows (client-side, immediate)
* 2. Export All Filtered Results (server-side via Server Action, max 5000)
*
* Features:
* - Dropdown menu with export options
* - Disabled state when no data/selection
* - Loading state for server-side export
* - Warning when result set > 5000 rows
*/
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Download, Loader2 } from 'lucide-react';
import { generatePolicySettingsCsv, downloadCsv, generateCsvFilename } from '@/lib/utils/csv-export';
import { exportPolicySettingsCSV } from '@/lib/actions/policySettings';
import type { PolicySettingRow } from '@/lib/types/policy-table';
import { toast } from 'sonner';
interface ExportButtonProps {
selectedRows: PolicySettingRow[];
selectedCount: number;
totalCount: number;
// Filter state for server-side export
policyTypes?: string[];
searchQuery?: string;
sortBy?: string;
sortDir?: 'asc' | 'desc';
}
export function ExportButton({
selectedRows,
selectedCount,
totalCount,
policyTypes,
searchQuery,
sortBy,
sortDir,
}: ExportButtonProps) {
const [isExporting, setIsExporting] = useState(false);
const hasSelection = selectedCount > 0;
const hasData = totalCount > 0;
const exceedsLimit = totalCount > 5000;
// Client-side export: Export selected rows
const handleExportSelected = () => {
if (!hasSelection) return;
try {
const csvContent = generatePolicySettingsCsv(selectedRows);
const filename = generateCsvFilename('policy-settings', selectedCount);
downloadCsv(csvContent, filename);
toast.success(`Exported ${selectedCount} rows to ${filename}`);
} catch (error) {
console.error('Export error:', error);
toast.error('Failed to export selected rows');
}
};
// Server-side export: Export all filtered results
const handleExportAll = async () => {
setIsExporting(true);
try {
const result = await exportPolicySettingsCSV({
policyTypes: policyTypes && policyTypes.length > 0 ? policyTypes : undefined,
searchQuery: searchQuery || undefined,
sortBy: sortBy as 'settingName' | 'policyName' | 'policyType' | 'lastSyncedAt' | undefined,
sortDir,
maxRows: 5000,
});
if (result.success && result.csv && result.filename) {
downloadCsv(result.csv, result.filename);
toast.success(`Exported ${result.rowCount} rows to ${result.filename}`);
} else {
toast.error(result.error || 'Failed to export data');
}
} catch (error) {
console.error('Export error:', error);
toast.error('An unexpected error occurred during export');
} finally {
setIsExporting(false);
}
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8"
disabled={!hasData || isExporting}
>
{isExporting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Exporting...
</>
) : (
<>
<Download className="mr-2 h-4 w-4" />
Export
</>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[220px]">
<DropdownMenuLabel>Export to CSV</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleExportSelected}
disabled={!hasSelection}
>
<div className="flex flex-col gap-1">
<span className="font-medium">Export Selected</span>
<span className="text-xs text-muted-foreground">
{hasSelection ? `${selectedCount} rows` : 'No rows selected'}
</span>
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleExportAll}
disabled={!hasData || isExporting}
>
<div className="flex flex-col gap-1">
<span className="font-medium">Export All Filtered</span>
<span className="text-xs text-muted-foreground">
{exceedsLimit
? `${totalCount} rows (limited to 5000)`
: `${totalCount} rows`}
</span>
</div>
</DropdownMenuItem>
{exceedsLimit && (
<>
<DropdownMenuSeparator />
<div className="px-2 py-1.5 text-xs text-amber-600">
Results exceed 5000 rows. Export will be limited.
</div>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}