Skip to content

Commit 706c35b

Browse files
authored
Merge pull request #258 from valkey-io/search-connection-and-cluster-dropdown
enable searching connections in the connection view and the cluster d…
2 parents 3a4cb20 + b2059c5 commit 706c35b

File tree

11 files changed

+183
-64
lines changed

11 files changed

+183
-64
lines changed

apps/frontend/src/components/cluster-topology/Cluster.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useState } from "react"
22
import { useSelector } from "react-redux"
33
import { Server, CheckCircle2 } from "lucide-react"
44
import { useParams } from "react-router"
5-
import { CONNECTED } from "@common/src/constants.ts"
5+
import { CONNECTED, MAX_CONNECTIONS } from "@common/src/constants.ts"
66
import { AppHeader } from "../ui/app-header"
77
import RouteContainer from "../ui/route-container"
88
import { StatCard } from "../ui/stat-card"
@@ -62,6 +62,8 @@ export function Cluster() {
6262
})
6363
})
6464

65+
const highlight = searchQuery && filteredEntries.length < MAX_CONNECTIONS ? searchQuery : ""
66+
6567
return (
6668
<RouteContainer className="overflow-y-hidden" title="Cluster Topology">
6769
<AppHeader icon={<Server size={20} />} title="Cluster Topology" />
@@ -110,6 +112,7 @@ export function Cluster() {
110112
return (
111113
<ClusterNode
112114
clusterId={clusterId!}
115+
highlight={highlight}
113116
key={primaryKey}
114117
primary={primary}
115118
primaryData={primaryData}

apps/frontend/src/components/cluster-topology/cluster-node.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Badge } from "../ui/badge"
77
import { CustomTooltip } from "../ui/tooltip"
88
import { Button } from "../ui/button"
99
import { Typography } from "../ui/typography"
10+
import { HighlightSearchMatch } from "../ui/highlight-search-match"
1011
import type { RootState } from "@/store.ts"
1112
import type { PrimaryNode, ParsedNodeInfo } from "@/state/valkey-features/cluster/clusterSlice"
1213
import { connectPending, type ConnectionDetails } from "@/state/valkey-features/connection/connectionSlice.ts"
@@ -20,13 +21,15 @@ interface ClusterNodeProps {
2021
primary: PrimaryNode
2122
primaryData: ParsedNodeInfo
2223
clusterId: string
24+
highlight?: string
2325
}
2426

2527
export function ClusterNode({
2628
primaryKey,
2729
primary,
2830
primaryData,
2931
clusterId,
32+
highlight = "",
3033
}: ClusterNodeProps) {
3134
const navigate = useNavigate()
3235
const dispatch = useAppDispatch()
@@ -83,12 +86,14 @@ export function ClusterNode({
8386
<Server className="text-primary shrink-0" size={18} />
8487
<div className="flex flex-col gap-1 min-w-0">
8588
<div className="flex items-center gap-2 flex-wrap">
86-
<Typography variant={"label"}>{primaryData?.server_name || primaryKey}</Typography>
89+
<Typography variant={"label"}>
90+
<HighlightSearchMatch query={highlight} text={primaryData?.server_name || primaryKey} />
91+
</Typography>
8792
<Badge className="text-xs px-2 py-0" variant={isConnected ? "success" : "secondary"}>
8893
PRIMARY
8994
</Badge>
9095
</div>
91-
<Typography variant="bodyXs">{`${primary.host}:${primary.port}`}</Typography>
96+
<Typography variant="bodyXs"><HighlightSearchMatch query={highlight} text={`${primary.host}:${primary.port}`} /></Typography>
9297
<NodeDetails nodeData={primaryData} />
9398
</div>
9499
</div>
@@ -109,7 +114,9 @@ export function ClusterNode({
109114
return (
110115
<div className="flex items-center mb-2 gap-1" key={replicaKey}>
111116
<Server className="text-primary shrink-0" size={16} />
112-
<Typography className="underline" variant="bodyXs">{replicaKey}</Typography>
117+
<Typography className="underline" variant="bodyXs">
118+
<HighlightSearchMatch query={highlight} text={replicaKey} />
119+
</Typography>
113120
</div>
114121
)
115122
})}

apps/frontend/src/components/connection/ClusterConnectionGroup.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ import { useAppDispatch } from "@/hooks/hooks.ts"
2222
import { Button } from "@/components/ui/button.tsx"
2323
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip.tsx"
2424
import { Typography } from "@/components/ui/typography.tsx"
25+
import { HighlightSearchMatch } from "@/components/ui/highlight-search-match.tsx"
2526

2627
interface ClusterConnectionGroupProps {
2728
clusterId: string
2829
connections: Array<{ connectionId: string; connection: ConnectionState }>
30+
highlight?: string
2931
onEdit?: (connectionId: string) => void
3032
}
3133

@@ -39,7 +41,7 @@ const getLatestTimestamp = R.pipe(
3941
// storage key for persisting open/closed state of cluster groups
4042
const getStorageKey = (clusterId: string) => `cluster-group-open-${clusterId}`
4143

42-
export const ClusterConnectionGroup = ({ clusterId, connections, onEdit }: ClusterConnectionGroupProps) => {
44+
export const ClusterConnectionGroup = ({ clusterId, connections, highlight = "", onEdit }: ClusterConnectionGroupProps) => {
4345
const dispatch = useAppDispatch()
4446
const [isOpen, setIsOpen] = useState(() => {
4547
const stored = localStorage.getItem(getStorageKey(clusterId))
@@ -147,7 +149,7 @@ export const ClusterConnectionGroup = ({ clusterId, connections, onEdit }: Clust
147149
title={firstNodeAlias || clusterId}
148150
to={`/${clusterId}/${firstConnectedConnection.connectionId}/cluster-topology`}
149151
>
150-
{firstNodeAlias || clusterId}
152+
<HighlightSearchMatch query={highlight} text={firstNodeAlias || clusterId} />
151153
</Link>
152154
</Typography>
153155
</div>
@@ -164,7 +166,7 @@ export const ClusterConnectionGroup = ({ clusterId, connections, onEdit }: Clust
164166
title={firstNodeAlias || clusterId}
165167
variant="code"
166168
>
167-
{firstNodeAlias || clusterId}
169+
<HighlightSearchMatch query={highlight} text={firstNodeAlias || clusterId} />
168170
</Typography>
169171
</div>
170172
)}
@@ -236,6 +238,7 @@ export const ClusterConnectionGroup = ({ clusterId, connections, onEdit }: Clust
236238
connection={connection}
237239
connectionId={connectionId}
238240
hideOpenButton={true}
241+
highlight={highlight}
239242
isNested={true}
240243
key={connectionId}
241244
onEdit={onEdit}

apps/frontend/src/components/connection/Connection.tsx

Lines changed: 81 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
import { useState } from "react"
22
import { useSelector } from "react-redux"
33
import { HousePlug } from "lucide-react"
4+
import { MAX_CONNECTIONS } from "@common/src/constants.ts"
45
import ConnectionForm from "../ui/connection-form.tsx"
56
import EditForm from "../ui/edit-form.tsx"
67
import RouteContainer from "../ui/route-container.tsx"
78
import { Button } from "../ui/button.tsx"
89
import { EmptyState } from "../ui/empty-state.tsx"
10+
import { SearchInput } from "../ui/search-input.tsx"
911
import { Typography } from "../ui/typography.tsx"
1012
import type { ConnectionState } from "@/state/valkey-features/connection/connectionSlice.ts"
1113
import { selectConnections } from "@/state/valkey-features/connection/connectionSelectors.ts"
1214
import { ConnectionEntry } from "@/components/connection/ConnectionEntry.tsx"
1315
import { ClusterConnectionGroup } from "@/components/connection/ClusterConnectionGroup.tsx"
1416

17+
const matchesSearch = (q: string, connection: ConnectionState) =>
18+
connection.searchableText.includes(q)
19+
1520
export function Connection() {
1621
const [showConnectionForm, setShowConnectionForm] = useState(false)
1722
const [showEditForm, setShowEditForm] = useState(false)
1823
const [editingConnectionId, setEditingConnectionId] = useState<string | undefined>(undefined)
24+
const [searchQuery, setSearchQuery] = useState("")
1925
const connections = useSelector(selectConnections)
2026

2127
const handleEditConnection = (connectionId: string) => {
@@ -51,16 +57,37 @@ export function Connection() {
5157
{ clusterGroups: {}, standaloneConnections: [] },
5258
)
5359

54-
const hasClusterGroups = Object.keys(clusterGroups).length > 0
55-
const hasStandaloneConnections = standaloneConnections.length > 0
5660
const hasConnectionsWithHistory = connectionsWithHistory.length > 0
5761

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+
5885
return (
5986
<RouteContainer title="connection">
6087
{/* top header */}
6188
<div className="flex items-center justify-between h-10">
6289
<Typography className="flex items-center gap-2" variant="heading">
63-
<HousePlug size={20}/> Connections
90+
<HousePlug size={20} /> Connections
6491
</Typography>
6592
{hasConnectionsWithHistory && (
6693
<Button
@@ -91,41 +118,59 @@ export function Connection() {
91118
title="You Have No Connections!"
92119
/>
93120
) : (
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}"
108133
</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+
)}
111153

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></>
129174
)}
130175
</RouteContainer>
131176
)

apps/frontend/src/components/connection/ConnectionEntry.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Button } from "@/components/ui/button.tsx"
1111
import { ConnectionStatusBadge } from "@/components/ui/connection-status-badge"
1212
import { ConnectionActionButtons } from "@/components/ui/connection-action-buttons.tsx"
1313
import { Typography } from "@/components/ui/typography.tsx"
14+
import { HighlightSearchMatch } from "@/components/ui/highlight-search-match"
1415
import { cn } from "@/lib/utils.ts"
1516
import history from "@/history.ts"
1617
import { useAppDispatch } from "@/hooks/hooks.ts"
@@ -21,6 +22,7 @@ interface ConnectionEntryProps {
2122
clusterId?: string
2223
hideOpenButton?: boolean
2324
isNested?: boolean
25+
highlight?: string
2426
onEdit?: (connectionId: string) => void
2527
}
2628

@@ -30,6 +32,7 @@ export const ConnectionEntry = ({
3032
clusterId,
3133
hideOpenButton = false,
3234
isNested = false,
35+
highlight = "",
3336
onEdit,
3437
}: ConnectionEntryProps) => {
3538
const dispatch = useAppDispatch()
@@ -73,7 +76,7 @@ export const ConnectionEntry = ({
7376
variant="link"
7477
>
7578
<Link title={label} to={clusterId ? `/${clusterId}/${connectionId}/dashboard` : `/${connectionId}/dashboard`}>
76-
{label}
79+
<HighlightSearchMatch query={highlight} text={label} />
7780
</Link>
7881
</Button>
7982
</Typography>
@@ -114,7 +117,7 @@ export const ConnectionEntry = ({
114117
variant="link"
115118
>
116119
<Link title={aliasLabel} to={clusterId ? `/${clusterId}/${connectionId}/dashboard` : `/${connectionId}/dashboard`}>
117-
{aliasLabel}
120+
<HighlightSearchMatch query={highlight} text={aliasLabel}/>
118121
</Link>
119122
</Button>
120123
</Typography>

apps/frontend/src/components/ui/accordion.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export default function Accordion({ accordionName, accordionItems, valueType = "
9696
{formatKey(key)}
9797
</Typography>
9898
{singleMetricDescriptions[key] && (
99-
<TooltipIcon description={`${singleMetricDescriptions[key].description} ${singleMetricDescriptions[key].unit}` } size={13} />
99+
<TooltipIcon description={singleMetricDescriptions[key].description} size={13} />
100100
)}
101101
</div>
102102
<Typography variant="bodySm">{formatMetricValue(key, value, valueType)}</Typography>

0 commit comments

Comments
 (0)