tenantpilot/lib/utils/csv-export.ts
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

142 lines
3.9 KiB
TypeScript

/**
* CSV Export Utilities
*
* Client-side CSV generation for policy settings export.
*
* Features:
* - RFC 4180 compliant CSV formatting
* - Proper escaping (commas, quotes, newlines)
* - UTF-8 BOM for Excel compatibility
* - Efficient string building
*/
import type { PolicySettingRow } from '@/lib/types/policy-table';
/**
* Escape a CSV field value according to RFC 4180
* - Wrap in quotes if contains comma, quote, or newline
* - Double any quotes inside the value
*/
function escapeCsvField(value: string | null | undefined): string {
if (value === null || value === undefined) {
return '';
}
const stringValue = String(value);
// Check if escaping is needed
const needsEscaping = stringValue.includes(',') ||
stringValue.includes('"') ||
stringValue.includes('\n') ||
stringValue.includes('\r');
if (!needsEscaping) {
return stringValue;
}
// Double any quotes and wrap in quotes
const escaped = stringValue.replace(/"/g, '""');
return `"${escaped}"`;
}
/**
* Generate CSV content from policy settings
* @param rows Array of policy settings to export
* @param columns Optional array of column keys to include (default: all)
* @returns CSV content as string with UTF-8 BOM
*/
export function generatePolicySettingsCsv(
rows: PolicySettingRow[],
columns?: Array<keyof PolicySettingRow>
): string {
// Default columns in preferred order
const defaultColumns: Array<keyof PolicySettingRow> = [
'settingName',
'settingValue',
'policyName',
'policyType',
'graphPolicyId',
'lastSyncedAt',
];
const columnsToExport = columns || defaultColumns;
// Column headers (human-readable)
const headers: Record<keyof PolicySettingRow, string> = {
id: 'ID',
tenantId: 'Tenant ID',
settingName: 'Setting Name',
settingValue: 'Setting Value',
policyName: 'Policy Name',
policyType: 'Policy Type',
graphPolicyId: 'Graph Policy ID',
lastSyncedAt: 'Last Synced',
createdAt: 'Created At',
};
// Build CSV rows
const csvLines: string[] = [];
// Header row
const headerRow = columnsToExport.map(col => escapeCsvField(headers[col])).join(',');
csvLines.push(headerRow);
// Data rows
for (const row of rows) {
const dataRow = columnsToExport.map(col => {
const value = row[col];
// Format dates
if (value instanceof Date) {
return escapeCsvField(value.toISOString());
}
// Format other values
return escapeCsvField(String(value));
}).join(',');
csvLines.push(dataRow);
}
// Join with newlines and add UTF-8 BOM for Excel compatibility
const csvContent = csvLines.join('\n');
const utf8Bom = '\uFEFF';
return utf8Bom + csvContent;
}
/**
* Trigger browser download of CSV content
* @param csvContent CSV content string
* @param filename Suggested filename (e.g., "policy-settings.csv")
*/
export function downloadCsv(csvContent: string, filename: string): void {
// Create blob with proper MIME type
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
// Create download link
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
// Trigger download
document.body.appendChild(link);
link.click();
// Cleanup
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
/**
* Generate filename with timestamp
* @param prefix Filename prefix (e.g., "policy-settings")
* @param selectedCount Optional count of selected rows
* @returns Filename with timestamp (e.g., "policy-settings-2025-12-10.csv")
*/
export function generateCsvFilename(prefix: string, selectedCount?: number): string {
const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
const suffix = selectedCount ? `-selected-${selectedCount}` : '';
return `${prefix}${suffix}-${date}.csv`;
}