diff --git a/apps/frontend/src/components/connection/ClusterConnectionGroup.tsx b/apps/frontend/src/components/connection/ClusterConnectionGroup.tsx new file mode 100644 index 00000000..4e29c92e --- /dev/null +++ b/apps/frontend/src/components/connection/ClusterConnectionGroup.tsx @@ -0,0 +1,111 @@ +import { useState, useRef, useEffect } from "react" +import { ChevronDown, ChevronRight, Network, CircleChevronRight } from "lucide-react" +import { CONNECTED } from "@common/src/constants.ts" +import { ConnectionEntry } from "./ConnectionEntry.tsx" +import type { ConnectionState } from "@/state/valkey-features/connection/connectionSlice" +import history from "@/history.ts" + +interface ClusterConnectionGroupProps { + clusterId: string + connections: Array<{ connectionId: string; connection: ConnectionState }> +} + +export const ClusterConnectionGroup = ({ clusterId, connections }: ClusterConnectionGroupProps) => { + const [isOpen, setIsOpen] = useState(false) + const dropdownRef = useRef(null) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + } + }, [isOpen]) + + const connected = connections.filter(({ connection }) => connection.status === CONNECTED) + const connectedCount = connected.length + const hasConnectedInstance = connectedCount > 0 + const firstConnectedConnection = connected[0] + + const handleOpenCluster = () => { + if (firstConnectedConnection) { + history.navigate(`/${clusterId}/${firstConnectedConnection.connectionId}/cluster-topology`) + } + } + + return ( +
+ {/* cluster head */} +
+
+
+ + +
+
+ +
+
+
+

{clusterId}

+ {hasConnectedInstance && ( + + {connectedCount} connected + + )} +
+

+ {connections.length} instance{connections.length !== 1 ? "s" : ""} +

+
+
+
+ +
+ {hasConnectedInstance && ( + + )} +
+
+
+ + {isOpen && ( +
+
+ {connections.map(({ connectionId, connection }) => ( + + ))} +
+
+ )} +
+ ) +} diff --git a/apps/frontend/src/components/connection/Connection.tsx b/apps/frontend/src/components/connection/Connection.tsx index 1cb52eac..f0c3150f 100644 --- a/apps/frontend/src/components/connection/Connection.tsx +++ b/apps/frontend/src/components/connection/Connection.tsx @@ -4,14 +4,35 @@ import { HousePlug } from "lucide-react" import * as R from "ramda" import ConnectionForm from "../ui/connection-form.tsx" import EditForm from "../ui/edit-form.tsx" +import type { ConnectionState } from "@/state/valkey-features/connection/connectionSlice.ts" import { selectConnections } from "@/state/valkey-features/connection/connectionSelectors.ts" -import { ConnectionEntry, ConnectionEntryHeader } from "@/components/connection/ConnectionEntry.tsx" +import { ConnectionEntry } from "@/components/connection/ConnectionEntry.tsx" +import { ClusterConnectionGroup } from "@/components/connection/ClusterConnectionGroup.tsx" export function Connection() { const [showConnectionForm, setShowConnectionForm] = useState(false) const [showEditForm, setShowEditForm] = useState(false) const connections = useSelector(selectConnections) + // grouping connections + const { clusterGroups, standaloneConnections } = Object.entries(connections).reduce<{ + clusterGroups: Record> + standaloneConnections: Array<{ connectionId: string; connection: ConnectionState }> + }>( + (acc, [connectionId, connection]) => { + const clusterId = connection.connectionDetails.clusterId + if (clusterId) + (acc.clusterGroups[clusterId] ??= []).push({ connectionId, connection }) + else + acc.standaloneConnections.push({ connectionId, connection }) + return acc + }, + { clusterGroups: {}, standaloneConnections: [] }, + ) + + const hasClusterGroups = Object.keys(clusterGroups).length > 0 + const hasStandaloneConnections = standaloneConnections.length > 0 + const connectButton = () => -
- { - isConnected && - <> - +
+ + {/* action buttons */} +
+ {isConnected && ( + <> + + + + )} + {(isDisconnected || !isConnected) && ( + + )} + -
+ + + ) + } + + // for standalone instnces + return ( +
+
+
+
+ +
+ +
+ -
+
+ + {/* action buttons */} +
+ {isConnected && ( + <> + {!hideOpenButton && ( + + )} + + + + )} + {(isDisconnected || !isConnected) && ( + - - } - { - (isDisconnected || !isConnected) && - - } - +
- +
) } diff --git a/apps/frontend/src/components/ui/app-sidebar.tsx b/apps/frontend/src/components/ui/app-sidebar.tsx index f25c022d..87957e94 100644 --- a/apps/frontend/src/components/ui/app-sidebar.tsx +++ b/apps/frontend/src/components/ui/app-sidebar.tsx @@ -1,6 +1,6 @@ import { LayoutDashboard, - Send, + SquareTerminal, HousePlug, ChevronLeft, ChevronRight, @@ -8,7 +8,7 @@ import { CircleQuestionMark, Github, Compass, - Server + Network } from "lucide-react" import { Link, useLocation, useParams } from "react-router" import { useState } from "react" @@ -62,14 +62,14 @@ export function AppSidebar() { }, { to: (clusterId ? `/${clusterId}/${id}/sendcommand` : `/${id}/sendcommand`), title: "Send Command", - icon: Send, + icon: SquareTerminal, }, ...(clusterId ? [ { to: `/${clusterId}/${id}/cluster-topology`, title: "Cluster Topology", - icon: Server, + icon: Network, }, ] : []),