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}
|
table={table}
|
||||||
density={preferences.density}
|
density={preferences.density}
|
||||||
onDensityChange={handleDensityChange}
|
onDensityChange={handleDensityChange}
|
||||||
|
selectedPolicyTypes={urlState.policyTypes}
|
||||||
|
onSelectedPolicyTypesChange={urlState.updatePolicyTypes}
|
||||||
|
searchQuery={urlState.searchQuery}
|
||||||
|
onSearchQueryChange={urlState.updateSearchQuery}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
|
|||||||
@ -12,7 +12,9 @@
|
|||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ColumnVisibilityMenu } from './ColumnVisibilityMenu';
|
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 { Table } from '@tanstack/react-table';
|
||||||
import type { PolicySettingRow } from '@/lib/types/policy-table';
|
import type { PolicySettingRow } from '@/lib/types/policy-table';
|
||||||
|
|
||||||
@ -20,17 +22,79 @@ interface PolicyTableToolbarProps {
|
|||||||
table: Table<PolicySettingRow>;
|
table: Table<PolicySettingRow>;
|
||||||
density: 'compact' | 'comfortable';
|
density: 'compact' | 'comfortable';
|
||||||
onDensityChange: (density: 'compact' | 'comfortable') => void;
|
onDensityChange: (density: 'compact' | 'comfortable') => void;
|
||||||
|
// Filter props
|
||||||
|
selectedPolicyTypes: string[];
|
||||||
|
onSelectedPolicyTypesChange: (types: string[]) => void;
|
||||||
|
searchQuery: string;
|
||||||
|
onSearchQueryChange: (query: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PolicyTableToolbar({
|
export function PolicyTableToolbar({
|
||||||
table,
|
table,
|
||||||
density,
|
density,
|
||||||
onDensityChange,
|
onDensityChange,
|
||||||
|
selectedPolicyTypes,
|
||||||
|
onSelectedPolicyTypesChange,
|
||||||
|
searchQuery,
|
||||||
|
onSearchQueryChange,
|
||||||
}: PolicyTableToolbarProps) {
|
}: PolicyTableToolbarProps) {
|
||||||
|
const hasActiveFilters = selectedPolicyTypes.length > 0 || searchQuery.length > 0;
|
||||||
|
|
||||||
|
const handleClearFilters = () => {
|
||||||
|
onSelectedPolicyTypesChange([]);
|
||||||
|
onSearchQueryChange('');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex flex-1 items-center space-x-2">
|
<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>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<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