11import { useQuery , useMutation , useQueryClient } from '@tanstack/react-query'
2+ import { Input , Select } from 'antd'
23import { apiClient } from '../lib/api'
34import { InstanceCard } from './InstanceCard'
45import { CreateInstanceDialog } from './CreateInstanceDialog'
56import { EditInstanceDialog } from './EditInstanceDialog'
67import { Button } from './ui/button'
7- import { Plus , RefreshCw } from 'lucide-react'
8+ import { Plus , RefreshCw , Search } from 'lucide-react'
89import { useState } from 'react'
910import { Segmented } from 'antd'
1011import 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