Skip to content

Commit 5f6bf48

Browse files
committed
feat: search and filter
1 parent f2fc5b4 commit 5f6bf48

File tree

1 file changed

+92
-25
lines changed

1 file changed

+92
-25
lines changed
Lines changed: 92 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
2+
import { Input, Select } from 'antd'
23
import { apiClient } from '../lib/api'
34
import { InstanceCard } from './InstanceCard'
45
import { CreateInstanceDialog } from './CreateInstanceDialog'
56
import { EditInstanceDialog } from './EditInstanceDialog'
67
import { Button } from './ui/button'
7-
import { Plus, RefreshCw } from 'lucide-react'
8+
import { Plus, RefreshCw, Search } from 'lucide-react'
89
import { useState } from 'react'
910
import { Segmented } from 'antd'
1011
import type { Instance } from '../types'
@@ -14,13 +15,34 @@ export function InstanceList() {
1415
const [showCreateDialog, setShowCreateDialog] = useState(false)
1516
const [editingInstance, setEditingInstance] = useState<Instance | null>(null)
1617
const [viewMode, setViewMode] = useState<'cards' | 'table'>('cards')
18+
const [searchTerm, setSearchTerm] = useState('')
19+
const [statusFilter, setStatusFilter] = useState('all')
20+
const [versionFilter, setVersionFilter] = useState('all')
1721
const queryClient = useQueryClient()
1822

1923
const { data: instances, isLoading, error, refetch } = useQuery({
2024
queryKey: ['instances'],
2125
queryFn: () => apiClient.getInstances(),
2226
})
2327

28+
// Compute unique versions
29+
const uniqueVersions = instances ? [...new Set(instances.map(inst => inst.gowa_version || 'latest'))].sort() : []
30+
31+
// Filtered instances
32+
const filteredInstances = instances
33+
? instances
34+
.filter(inst => statusFilter === 'all' || statusFilter === undefined || inst.status?.toLowerCase() === statusFilter.toLowerCase())
35+
.filter(inst => versionFilter === 'all' || versionFilter === undefined || inst.gowa_version === versionFilter)
36+
.filter(inst =>
37+
searchTerm === ''
38+
? true
39+
: inst.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
40+
inst.key.toLowerCase().includes(searchTerm.toLowerCase())
41+
)
42+
: []
43+
44+
const filteredCount = filteredInstances.length
45+
2446
const refreshMutation = useMutation({
2547
mutationFn: () => apiClient.getInstances(),
2648
onSuccess: () => {
@@ -30,18 +52,18 @@ export function InstanceList() {
3052

3153
if (isLoading) {
3254
return (
33-
<div className="flex items-center justify-center py-12">
34-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
55+
<div className="flex justify-center items-center py-12">
56+
<div className="w-8 h-8 rounded-full border-b-2 border-gray-900 animate-spin"></div>
3557
</div>
3658
)
3759
}
3860

3961
if (error) {
4062
return (
41-
<div className="text-center py-12">
42-
<p className="text-red-600 mb-4">Failed to load instances</p>
63+
<div className="py-12 text-center">
64+
<p className="mb-4 text-red-600">Failed to load instances</p>
4365
<Button onClick={() => refetch()} variant="outline">
44-
<RefreshCw className="h-4 w-4 mr-2" />
66+
<RefreshCw className="mr-2 w-4 h-4" />
4567
Retry
4668
</Button>
4769
</div>
@@ -50,26 +72,66 @@ export function InstanceList() {
5072

5173
return (
5274
<div className="space-y-6">
53-
<div className="space-y-3 mb-6">
54-
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
75+
{/* Filters and Search */}
76+
<div className="mb-6 space-y-3">
77+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
78+
<div className="relative flex-1">
79+
<Search className="absolute left-3 top-1/2 w-4 h-4 text-gray-400 transform -translate-y-1/2" />
80+
<Input
81+
placeholder="Search by name or key..."
82+
value={searchTerm}
83+
onChange={(e) => setSearchTerm(e.target.value)}
84+
prefix=""
85+
className="pl-10"
86+
/>
87+
</div>
88+
<Select
89+
placeholder="Filter by status"
90+
value={statusFilter}
91+
onChange={setStatusFilter}
92+
allowClear
93+
style={{ width: 150 }}
94+
>
95+
<Select.Option value="all">All Status</Select.Option>
96+
<Select.Option value="running">Running</Select.Option>
97+
<Select.Option value="stopped">Stopped</Select.Option>
98+
<Select.Option value="error">Error</Select.Option>
99+
</Select>
100+
<Select
101+
placeholder="Filter by version"
102+
value={versionFilter}
103+
onChange={setVersionFilter}
104+
allowClear
105+
style={{ width: 150 }}
106+
>
107+
<Select.Option value="all">All Versions</Select.Option>
108+
{uniqueVersions.map(version => (
109+
<Select.Option key={version} value={version}>{version}</Select.Option>
110+
))}
111+
</Select>
112+
</div>
113+
</div>
114+
115+
<div className="mb-6 space-y-3">
116+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
55117
<div className="flex items-center space-x-2">
56118
<Button onClick={() => setShowCreateDialog(true)}>
57-
<Plus className="h-4 w-4 mr-2" />
119+
<Plus className="mr-2 w-4 h-4" />
58120
Create Instance
59121
</Button>
60-
<Button
61-
variant="outline"
122+
<Button
123+
variant="outline"
62124
onClick={() => refreshMutation.mutate()}
63125
disabled={refreshMutation.isPending}
64126
>
65127
<RefreshCw className={`h-4 w-4 mr-2 ${refreshMutation.isPending ? 'animate-spin' : ''}`} />
66128
Refresh
67129
</Button>
68130
</div>
69-
<div className="flex items-center gap-4">
70-
{instances && (
71-
<span className="inline-flex items-center h-8 text-sm text-gray-600 sm:text-right m-0">
72-
{instances.length} instance{instances.length !== 1 ? 's' : ''}
131+
<div className="flex gap-4 items-center">
132+
{filteredInstances && (
133+
<span className="inline-flex items-center m-0 h-8 text-sm text-gray-600 sm:text-right">
134+
{filteredCount} instance{filteredCount !== 1 ? 's' : ''}
73135
</span>
74136
)}
75137
<Segmented
@@ -81,28 +143,33 @@ export function InstanceList() {
81143
</div>
82144
</div>
83145

84-
{instances && instances.length > 0 ? (
146+
{filteredInstances.length > 0 ? (
85147
viewMode === 'cards' ? (
86-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
87-
{instances.map((instance) => (
148+
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
149+
{filteredInstances.map((instance) => (
88150
<InstanceCard key={instance.id} instance={instance} />
89151
))}
90152
</div>
91153
) : (
92-
<InstanceTable instances={instances as Instance[]} onEdit={setEditingInstance} />
154+
<InstanceTable instances={filteredInstances as Instance[]} onEdit={setEditingInstance} />
93155
)
94156
) : (
95-
<div className="text-center py-12 bg-white rounded-lg border border-gray-200">
96-
<p className="text-gray-600 mb-4">No instances found</p>
157+
<div className="py-12 text-center bg-white rounded-lg border border-gray-200">
158+
<p className="mb-4 text-gray-600">
159+
{searchTerm || statusFilter !== 'all' || versionFilter !== 'all'
160+
? 'No instances match the current filters'
161+
: 'No instances found'
162+
}
163+
</p>
97164
<Button onClick={() => setShowCreateDialog(true)}>
98-
<Plus className="h-4 w-4 mr-2" />
165+
<Plus className="mr-2 w-4 h-4" />
99166
Create your first instance
100167
</Button>
101168
</div>
102169
)}
103170

104-
<CreateInstanceDialog
105-
open={showCreateDialog}
171+
<CreateInstanceDialog
172+
open={showCreateDialog}
106173
onOpenChange={setShowCreateDialog}
107174
/>
108175
{editingInstance && (
@@ -116,4 +183,4 @@ export function InstanceList() {
116183
)}
117184
</div>
118185
)
119-
}
186+
}

0 commit comments

Comments
 (0)