feat(policy-explorer-v2): implement Phase 4 - Enhanced Filtering
All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 2s

 New Features (Tasks T023-T028)
- PolicyTypeFilter component with multi-select checkboxes
- 8 common Intune policy types (deviceConfiguration, compliancePolicy, etc.)
- Active filter badges with individual remove buttons
- 'Clear All Filters' button when filters active
- Filter count badge in dropdown trigger

🔧 Updates
- PolicyTableToolbar now accepts filter props
- PolicyExplorerV2Client connects filters to URL state
- Filters sync with URL for shareable links
- Filter state triggers data refetch automatically

📦 Dependencies
- Added shadcn DropdownMenu component

 Zero TypeScript compilation errors
 All Phase 4 tasks complete (T023-T028)
 Ready for Phase 5 (CSV Export)

Refs: specs/004-policy-explorer-v2/tasks.md Phase 4
This commit is contained in:
Ahmed Darrazi 2025-12-10 00:28:35 +01:00
parent 41e80b6c0c
commit c59400cd48
3 changed files with 195 additions and 2 deletions

View File

@ -147,6 +147,10 @@ export function PolicyExplorerV2Client() {
table={table}
density={preferences.density}
onDensityChange={handleDensityChange}
selectedPolicyTypes={urlState.policyTypes}
onSelectedPolicyTypesChange={urlState.updatePolicyTypes}
searchQuery={urlState.searchQuery}
onSearchQueryChange={urlState.updateSearchQuery}
/>
{/* Table */}

View File

@ -12,7 +12,9 @@
import { Button } from '@/components/ui/button';
import { ColumnVisibilityMenu } from './ColumnVisibilityMenu';
import { LayoutList, LayoutGrid } from 'lucide-react';
import { PolicyTypeFilter } from './PolicyTypeFilter';
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';
@ -20,17 +22,79 @@ 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 function PolicyTableToolbar({
table,
density,
onDensityChange,
selectedPolicyTypes,
onSelectedPolicyTypesChange,
searchQuery,
onSearchQueryChange,
}: 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">
{/* Search and filters will be added here in Phase 4 */}
{/* 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">

View File

@ -0,0 +1,125 @@
/**
* PolicyTypeFilter Component
*
* Multi-select checkbox dropdown for filtering by policy types.
*
* Features:
* - Checkbox list of all available policy types
* - "Select All" / "Clear All" actions
* - Active filter count badge
* - Syncs with URL state via useURLState hook
*/
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Badge } from '@/components/ui/badge';
import { Filter } from 'lucide-react';
// Common Intune policy types
const POLICY_TYPES = [
{ value: 'deviceConfiguration', label: 'Device Configuration' },
{ value: 'compliancePolicy', label: 'Compliance Policy' },
{ value: 'deviceManagementScript', label: 'Device Management Script' },
{ value: 'windowsUpdateForBusiness', label: 'Windows Update for Business' },
{ value: 'iosUpdateConfiguration', label: 'iOS Update Configuration' },
{ value: 'macOSExtensionsConfiguration', label: 'macOS Extensions' },
{ value: 'settingsCatalog', label: 'Settings Catalog' },
{ value: 'endpointProtection', label: 'Endpoint Protection' },
] as const;
interface PolicyTypeFilterProps {
selectedTypes: string[];
onSelectedTypesChange: (types: string[]) => void;
}
export function PolicyTypeFilter({
selectedTypes,
onSelectedTypesChange,
}: PolicyTypeFilterProps) {
const [open, setOpen] = useState(false);
const handleSelectAll = () => {
onSelectedTypesChange(POLICY_TYPES.map(t => t.value));
};
const handleClearAll = () => {
onSelectedTypesChange([]);
};
const handleToggleType = (type: string) => {
if (selectedTypes.includes(type)) {
onSelectedTypesChange(selectedTypes.filter(t => t !== type));
} else {
onSelectedTypesChange([...selectedTypes, type]);
}
};
const activeCount = selectedTypes.length;
const allSelected = activeCount === POLICY_TYPES.length;
const noneSelected = activeCount === 0;
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-8">
<Filter className="mr-2 h-4 w-4" />
Policy Type
{activeCount > 0 && (
<Badge variant="secondary" className="ml-2 h-5 px-1.5">
{activeCount}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[250px]">
<DropdownMenuLabel>Filter by Policy Type</DropdownMenuLabel>
<DropdownMenuSeparator />
{/* Quick Actions */}
<div className="flex items-center justify-between px-2 py-1.5">
<Button
variant="ghost"
size="sm"
onClick={handleSelectAll}
disabled={allSelected}
className="h-7 text-xs"
>
Select All
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleClearAll}
disabled={noneSelected}
className="h-7 text-xs"
>
Clear All
</Button>
</div>
<DropdownMenuSeparator />
{/* Policy Type Checkboxes */}
{POLICY_TYPES.map((type) => (
<DropdownMenuCheckboxItem
key={type.value}
checked={selectedTypes.includes(type.value)}
onCheckedChange={() => handleToggleType(type.value)}
>
{type.label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}