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
150 lines
4.2 KiB
TypeScript
150 lines
4.2 KiB
TypeScript
/**
|
|
* PolicyTableToolbar
|
|
*
|
|
* Toolbar above the data table with:
|
|
* - Column visibility menu
|
|
* - Density mode toggle (compact/comfortable)
|
|
* - Export button (added later in Phase 5)
|
|
* - Filter controls (added later in Phase 4)
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
import { ColumnVisibilityMenu } from './ColumnVisibilityMenu';
|
|
import { PolicyTypeFilter } from './PolicyTypeFilter';
|
|
import { ExportButton } from './ExportButton';
|
|
import { LayoutList, LayoutGrid, X } from 'lucide-react';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import type { Table } from '@tanstack/react-table';
|
|
import type { PolicySettingRow } from '@/lib/types/policy-table';
|
|
|
|
interface PolicyTableToolbarProps {
|
|
table: Table<PolicySettingRow>;
|
|
density: 'compact' | 'comfortable';
|
|
onDensityChange: (density: 'compact' | 'comfortable') => void;
|
|
// Filter props
|
|
selectedPolicyTypes: string[];
|
|
onSelectedPolicyTypesChange: (types: string[]) => void;
|
|
searchQuery: string;
|
|
onSearchQueryChange: (query: string) => void;
|
|
// Export props
|
|
selectedRows: PolicySettingRow[];
|
|
selectedCount: number;
|
|
totalCount: number;
|
|
sortBy?: string;
|
|
sortDir?: 'asc' | 'desc';
|
|
}
|
|
|
|
export function PolicyTableToolbar({
|
|
table,
|
|
density,
|
|
onDensityChange,
|
|
selectedPolicyTypes,
|
|
onSelectedPolicyTypesChange,
|
|
searchQuery,
|
|
onSearchQueryChange,
|
|
selectedRows,
|
|
selectedCount,
|
|
totalCount,
|
|
sortBy,
|
|
sortDir,
|
|
}: PolicyTableToolbarProps) {
|
|
const hasActiveFilters = selectedPolicyTypes.length > 0 || searchQuery.length > 0;
|
|
|
|
const handleClearFilters = () => {
|
|
onSelectedPolicyTypesChange([]);
|
|
onSearchQueryChange('');
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex flex-1 items-center space-x-2">
|
|
{/* Policy Type Filter */}
|
|
<PolicyTypeFilter
|
|
selectedTypes={selectedPolicyTypes}
|
|
onSelectedTypesChange={onSelectedPolicyTypesChange}
|
|
/>
|
|
|
|
{/* Active Filter Badges */}
|
|
{selectedPolicyTypes.length > 0 && (
|
|
<div className="flex items-center gap-1">
|
|
{selectedPolicyTypes.slice(0, 2).map((type) => (
|
|
<Badge
|
|
key={type}
|
|
variant="secondary"
|
|
className="h-6 px-2 text-xs"
|
|
>
|
|
{type}
|
|
<button
|
|
onClick={() =>
|
|
onSelectedPolicyTypesChange(
|
|
selectedPolicyTypes.filter((t) => t !== type)
|
|
)
|
|
}
|
|
className="ml-1 hover:text-destructive"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</button>
|
|
</Badge>
|
|
))}
|
|
{selectedPolicyTypes.length > 2 && (
|
|
<Badge variant="secondary" className="h-6 px-2 text-xs">
|
|
+{selectedPolicyTypes.length - 2} more
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Clear All Filters Button */}
|
|
{hasActiveFilters && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleClearFilters}
|
|
className="h-8 px-2 lg:px-3"
|
|
>
|
|
Clear filters
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
{/* Density Toggle */}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-8"
|
|
onClick={() => onDensityChange(density === 'compact' ? 'comfortable' : 'compact')}
|
|
>
|
|
{density === 'compact' ? (
|
|
<>
|
|
<LayoutGrid className="mr-2 h-4 w-4" />
|
|
Comfortable
|
|
</>
|
|
) : (
|
|
<>
|
|
<LayoutList className="mr-2 h-4 w-4" />
|
|
Compact
|
|
</>
|
|
)}
|
|
</Button>
|
|
|
|
{/* Column Visibility Menu */}
|
|
<ColumnVisibilityMenu table={table} />
|
|
|
|
{/* Export Button */}
|
|
<ExportButton
|
|
selectedRows={selectedRows}
|
|
selectedCount={selectedCount}
|
|
totalCount={totalCount}
|
|
policyTypes={selectedPolicyTypes}
|
|
searchQuery={searchQuery}
|
|
sortBy={sortBy}
|
|
sortDir={sortDir}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|