feat(policy-explorer-v2): implement Phase 4 - Enhanced Filtering
All checks were successful
Trigger Cloudarix Deploy / call-webhook (push) Successful in 2s
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:
parent
41e80b6c0c
commit
c59400cd48
@ -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 */}
|
||||
|
||||
@ -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">
|
||||
|
||||
125
components/policy-explorer/PolicyTypeFilter.tsx
Normal file
125
components/policy-explorer/PolicyTypeFilter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user