|
1 | 1 | import { useState } from "react" |
2 | 2 | import { useSelector } from "react-redux" |
3 | 3 | import { HousePlug } from "lucide-react" |
| 4 | +import { MAX_CONNECTIONS } from "@common/src/constants.ts" |
4 | 5 | import ConnectionForm from "../ui/connection-form.tsx" |
5 | 6 | import EditForm from "../ui/edit-form.tsx" |
6 | 7 | import RouteContainer from "../ui/route-container.tsx" |
7 | 8 | import { Button } from "../ui/button.tsx" |
8 | 9 | import { EmptyState } from "../ui/empty-state.tsx" |
| 10 | +import { SearchInput } from "../ui/search-input.tsx" |
9 | 11 | import { Typography } from "../ui/typography.tsx" |
10 | 12 | import type { ConnectionState } from "@/state/valkey-features/connection/connectionSlice.ts" |
11 | 13 | import { selectConnections } from "@/state/valkey-features/connection/connectionSelectors.ts" |
12 | 14 | import { ConnectionEntry } from "@/components/connection/ConnectionEntry.tsx" |
13 | 15 | import { ClusterConnectionGroup } from "@/components/connection/ClusterConnectionGroup.tsx" |
14 | 16 |
|
| 17 | +const matchesSearch = (q: string, connection: ConnectionState) => |
| 18 | + connection.searchableText.includes(q) |
| 19 | + |
15 | 20 | export function Connection() { |
16 | 21 | const [showConnectionForm, setShowConnectionForm] = useState(false) |
17 | 22 | const [showEditForm, setShowEditForm] = useState(false) |
18 | 23 | const [editingConnectionId, setEditingConnectionId] = useState<string | undefined>(undefined) |
| 24 | + const [searchQuery, setSearchQuery] = useState("") |
19 | 25 | const connections = useSelector(selectConnections) |
20 | 26 |
|
21 | 27 | const handleEditConnection = (connectionId: string) => { |
@@ -51,16 +57,37 @@ export function Connection() { |
51 | 57 | { clusterGroups: {}, standaloneConnections: [] }, |
52 | 58 | ) |
53 | 59 |
|
54 | | - const hasClusterGroups = Object.keys(clusterGroups).length > 0 |
55 | | - const hasStandaloneConnections = standaloneConnections.length > 0 |
56 | 60 | const hasConnectionsWithHistory = connectionsWithHistory.length > 0 |
57 | 61 |
|
| 62 | + // Filter by search query |
| 63 | + const q = searchQuery.toLowerCase() |
| 64 | + const filteredClusterGroups: typeof clusterGroups = {} |
| 65 | + if (q) { |
| 66 | + for (const [clusterId, conns] of Object.entries(clusterGroups)) { |
| 67 | + const matched = conns.filter(({ connection }) => matchesSearch(q, connection)) |
| 68 | + if (matched.length > 0) filteredClusterGroups[clusterId] = matched |
| 69 | + } |
| 70 | + } |
| 71 | + const filteredStandaloneConnections = q |
| 72 | + ? standaloneConnections.filter(({ connection }) => matchesSearch(q, connection)) |
| 73 | + : standaloneConnections |
| 74 | + |
| 75 | + const hasFilteredClusters = q ? Object.keys(filteredClusterGroups).length > 0 : Object.keys(clusterGroups).length > 0 |
| 76 | + const hasFilteredStandalone = q ? filteredStandaloneConnections.length > 0 : standaloneConnections.length > 0 |
| 77 | + const hasAnyResults = hasFilteredClusters || hasFilteredStandalone |
| 78 | + const displayClusterGroups = q ? filteredClusterGroups : clusterGroups |
| 79 | + |
| 80 | + const totalResults = filteredStandaloneConnections.length + |
| 81 | + Object.values(displayClusterGroups).reduce((sum, conns) => sum + conns.length, 0) |
| 82 | + |
| 83 | + const highlight = q && totalResults < MAX_CONNECTIONS ? q : "" |
| 84 | + |
58 | 85 | return ( |
59 | 86 | <RouteContainer title="connection"> |
60 | 87 | {/* top header */} |
61 | 88 | <div className="flex items-center justify-between h-10"> |
62 | 89 | <Typography className="flex items-center gap-2" variant="heading"> |
63 | | - <HousePlug size={20}/> Connections |
| 90 | + <HousePlug size={20} /> Connections |
64 | 91 | </Typography> |
65 | 92 | {hasConnectionsWithHistory && ( |
66 | 93 | <Button |
@@ -91,41 +118,59 @@ export function Connection() { |
91 | 118 | title="You Have No Connections!" |
92 | 119 | /> |
93 | 120 | ) : ( |
94 | | - <div className="flex-1"> |
95 | | - {/* for clusters */} |
96 | | - {hasClusterGroups && ( |
97 | | - <div className="mb-8"> |
98 | | - <Typography className="mb-2" variant="bodyLg">Clusters</Typography> |
99 | | - <div> |
100 | | - {Object.entries(clusterGroups).map(([clusterId, clusterConnections]) => ( |
101 | | - <ClusterConnectionGroup |
102 | | - clusterId={clusterId} |
103 | | - connections={clusterConnections} |
104 | | - key={clusterId} |
105 | | - onEdit={handleEditConnection} |
106 | | - /> |
107 | | - ))} |
| 121 | + <> |
| 122 | + {/* Search */} |
| 123 | + <SearchInput |
| 124 | + onChange={(e) => setSearchQuery(e.target.value)} |
| 125 | + onClear={() => setSearchQuery("")} |
| 126 | + placeholder="Search connections by host, port, or alias..." |
| 127 | + value={searchQuery} |
| 128 | + /> |
| 129 | + <div className="flex-1 h-full border border-input rounded-md shadow-xs overflow-y-auto px-4 py-2"> |
| 130 | + {!hasAnyResults && q ? ( |
| 131 | + <div className="text-center py-8 text-muted-foreground min-h-40"> |
| 132 | + No connections match "{searchQuery}" |
108 | 133 | </div> |
109 | | - </div> |
110 | | - )} |
| 134 | + ) : ( |
| 135 | + <> |
| 136 | + {/* for clusters */} |
| 137 | + {hasFilteredClusters && ( |
| 138 | + <div className="mb-8"> |
| 139 | + <Typography className="mb-2" variant="bodyLg">Clusters</Typography> |
| 140 | + <div> |
| 141 | + {Object.entries(displayClusterGroups).map(([clusterId, clusterConnections]) => ( |
| 142 | + <ClusterConnectionGroup |
| 143 | + clusterId={clusterId} |
| 144 | + connections={clusterConnections} |
| 145 | + highlight={highlight} |
| 146 | + key={clusterId} |
| 147 | + onEdit={handleEditConnection} |
| 148 | + /> |
| 149 | + ))} |
| 150 | + </div> |
| 151 | + </div> |
| 152 | + )} |
111 | 153 |
|
112 | | - {/* for standalone instances */} |
113 | | - {hasStandaloneConnections && ( |
114 | | - <div> |
115 | | - <Typography className="mb-2" variant="bodyLg">Instances</Typography> |
116 | | - <div> |
117 | | - {standaloneConnections.map(({ connectionId, connection }) => ( |
118 | | - <ConnectionEntry |
119 | | - connection={connection} |
120 | | - connectionId={connectionId} |
121 | | - key={connectionId} |
122 | | - onEdit={handleEditConnection} |
123 | | - /> |
124 | | - ))} |
125 | | - </div> |
126 | | - </div> |
127 | | - )} |
128 | | - </div> |
| 154 | + {/* for standalone instances */} |
| 155 | + {hasFilteredStandalone && ( |
| 156 | + <div> |
| 157 | + <Typography className="mb-2" variant="bodyLg">Instances</Typography> |
| 158 | + <div> |
| 159 | + {filteredStandaloneConnections.map(({ connectionId, connection }) => ( |
| 160 | + <ConnectionEntry |
| 161 | + connection={connection} |
| 162 | + connectionId={connectionId} |
| 163 | + highlight={highlight} |
| 164 | + key={connectionId} |
| 165 | + onEdit={handleEditConnection} |
| 166 | + /> |
| 167 | + ))} |
| 168 | + </div> |
| 169 | + </div> |
| 170 | + )} |
| 171 | + </> |
| 172 | + )} |
| 173 | + </div></> |
129 | 174 | )} |
130 | 175 | </RouteContainer> |
131 | 176 | ) |
|
0 commit comments