diff --git a/app/processes/page.tsx b/app/processes/page.tsx index 69d415d..f225d74 100644 --- a/app/processes/page.tsx +++ b/app/processes/page.tsx @@ -1,19 +1,26 @@ 'use client'; -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { useProcesses } from '@/lib/hooks/useSupervisor'; import { ProcessCard } from '@/components/process/ProcessCard'; import { GroupView } from '@/components/groups/GroupView'; import { GroupSelector } from '@/components/groups/GroupSelector'; import { BatchActions } from '@/components/process/BatchActions'; +import { ProcessFilters } from '@/components/process/ProcessFilters'; import { RefreshCw, AlertCircle, CheckSquare } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import type { ProcessInfo } from '@/lib/supervisor/types'; export default function ProcessesPage() { const [viewMode, setViewMode] = useState<'flat' | 'grouped'>('flat'); const [selectedProcesses, setSelectedProcesses] = useState>(new Set()); + const [filteredProcesses, setFilteredProcesses] = useState([]); const { data: processes, isLoading, isError, refetch } = useProcesses(); + const handleFilterChange = useCallback((filtered: ProcessInfo[]) => { + setFilteredProcesses(filtered); + }, []); + const handleSelectionChange = (processId: string, selected: boolean) => { setSelectedProcesses((prev) => { const newSet = new Set(prev); @@ -27,11 +34,12 @@ export default function ProcessesPage() { }; const handleSelectAll = () => { - if (processes) { - if (selectedProcesses.size === processes.length) { + const displayedProcesses = filteredProcesses.length > 0 ? filteredProcesses : (processes || []); + if (displayedProcesses) { + if (selectedProcesses.size === displayedProcesses.length) { setSelectedProcesses(new Set()); } else { - setSelectedProcesses(new Set(processes.map((p) => `${p.group}:${p.name}`))); + setSelectedProcesses(new Set(displayedProcesses.map((p) => `${p.group}:${p.name}`))); } } }; @@ -72,17 +80,20 @@ export default function ProcessesPage() { ); } + const displayedProcesses = filteredProcesses.length > 0 || !processes ? filteredProcesses : processes; + return (

Processes

- {processes?.length ?? 0} processes configured + {displayedProcesses.length} of {processes?.length ?? 0} processes + {displayedProcesses.length !== (processes?.length ?? 0) && ' (filtered)'}

- {viewMode === 'flat' && processes && processes.length > 0 && ( + {viewMode === 'flat' && displayedProcesses.length > 0 && ( )} @@ -101,15 +112,25 @@ export default function ProcessesPage() {
+ {/* Filters */} + {processes && processes.length > 0 && ( + + )} + + {/* Process Display */} {processes && processes.length === 0 ? (

No processes configured

+ ) : displayedProcesses.length === 0 ? ( +
+

No processes match the current filters

+
) : viewMode === 'grouped' ? ( - + ) : (
- {processes?.map((process) => { + {displayedProcesses.map((process) => { const fullName = `${process.group}:${process.name}`; return (
diff --git a/components/process/ProcessFilters.tsx b/components/process/ProcessFilters.tsx new file mode 100644 index 0000000..5d6f60c --- /dev/null +++ b/components/process/ProcessFilters.tsx @@ -0,0 +1,177 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { ProcessInfo, ProcessState } from '@/lib/supervisor/types'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { Search, X, Filter } from 'lucide-react'; +import { cn } from '@/lib/utils/cn'; + +interface ProcessFiltersProps { + processes: ProcessInfo[]; + onFilterChange: (filtered: ProcessInfo[]) => void; +} + +const STATE_FILTERS = [ + { value: 'all', label: 'All States', color: '' }, + { value: 'running', label: 'Running', color: 'bg-success' }, + { value: 'stopped', label: 'Stopped', color: 'bg-muted-foreground' }, + { value: 'fatal', label: 'Fatal', color: 'bg-destructive' }, + { value: 'starting', label: 'Starting', color: 'bg-warning' }, + { value: 'stopping', label: 'Stopping', color: 'bg-accent' }, +]; + +export function ProcessFilters({ processes, onFilterChange }: ProcessFiltersProps) { + const [searchTerm, setSearchTerm] = useState(''); + const [selectedState, setSelectedState] = useState('all'); + const [selectedGroup, setSelectedGroup] = useState('all'); + const [showFilters, setShowFilters] = useState(false); + + // Extract unique groups from processes + const groups = Array.from(new Set(processes.map((p) => p.group))).sort(); + + useEffect(() => { + let filtered = [...processes]; + + // Filter by search term + if (searchTerm) { + const term = searchTerm.toLowerCase(); + filtered = filtered.filter( + (p) => + p.name.toLowerCase().includes(term) || + p.group.toLowerCase().includes(term) || + p.description.toLowerCase().includes(term) + ); + } + + // Filter by state + if (selectedState !== 'all') { + filtered = filtered.filter((p) => { + switch (selectedState) { + case 'running': + return p.state === ProcessState.RUNNING; + case 'stopped': + return p.state === ProcessState.STOPPED || p.state === ProcessState.EXITED; + case 'fatal': + return p.state === ProcessState.FATAL; + case 'starting': + return p.state === ProcessState.STARTING; + case 'stopping': + return p.state === ProcessState.STOPPING; + default: + return true; + } + }); + } + + // Filter by group + if (selectedGroup !== 'all') { + filtered = filtered.filter((p) => p.group === selectedGroup); + } + + onFilterChange(filtered); + }, [searchTerm, selectedState, selectedGroup, processes, onFilterChange]); + + const clearFilters = () => { + setSearchTerm(''); + setSelectedState('all'); + setSelectedGroup('all'); + }; + + const hasActiveFilters = searchTerm || selectedState !== 'all' || selectedGroup !== 'all'; + + return ( +
+ {/* Search Bar */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10 pr-10" + /> + {searchTerm && ( + + )} +
+ + {hasActiveFilters && ( + + )} +
+ + {/* Filter Panel */} + {showFilters && ( + + + {/* State Filter */} +
+ +
+ {STATE_FILTERS.map((filter) => ( + + ))} +
+
+ + {/* Group Filter */} + {groups.length > 1 && ( +
+ +
+ + {groups.map((group) => ( + + ))} +
+
+ )} +
+
+ )} +
+ ); +}