-
Notifications
You must be signed in to change notification settings - Fork 14
revamped the connection layout and added the cluster grouping #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import { useState, useRef, useEffect } from "react" | ||
| import { CONNECTED } from "@common/src/constants.ts" | ||
| import { ChevronDown, ChevronRight, Network, CircleChevronRight } from "lucide-react" | ||
| 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<HTMLDivElement>(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 hasConnectedInstance = connections.some(({ connection }) => connection.status === CONNECTED) | ||
| const connectedCount = connections.filter(({ connection }) => connection.status === CONNECTED).length | ||
|
|
||
| const firstConnectedConnection = connections.find(({ connection }) => connection.status === CONNECTED) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. while the impact is very minor, we can still iterate over
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adressed. |
||
|
|
||
| const handleOpenCluster = () => { | ||
| if (firstConnectedConnection) { | ||
| history.navigate(`/${clusterId}/${firstConnectedConnection.connectionId}/cluster-topology`) | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| className="mb-3 border dark:border-tw-dark-border rounded bg-white dark:bg-tw-dark-primary" | ||
| ref={dropdownRef} | ||
| > | ||
| {/* cluster head */} | ||
| <div className="p-4"> | ||
| <div className="flex items-center justify-between"> | ||
| <div className="flex items-center gap-3 flex-1"> | ||
| <button | ||
| className="p-1 rounded hover:bg-tw-primary/20" | ||
| onClick={() => setIsOpen(!isOpen)} | ||
| > | ||
| {isOpen ? <ChevronDown size={20} /> : <ChevronRight size={20} />} | ||
| </button> | ||
|
|
||
| <div className="flex items-center gap-2"> | ||
| <div className="p-2 bg-tw-primary/10 dark:bg-tw-primary/20 rounded"> | ||
| <Network className="text-tw-primary" size={18} /> | ||
| </div> | ||
| <div> | ||
| <div className="flex items-center gap-2"> | ||
| <h3 className="text-sm text-gray-900 dark:text-white">{clusterId}</h3> | ||
| {hasConnectedInstance && ( | ||
| <span className="px-2 py-0.5 text-xs font-medium bg-teal-100 dark:bg-teal-900/30 text-teal-700 | ||
| dark:text-teal-400 rounded-full"> | ||
| {connectedCount} connected | ||
| </span> | ||
| )} | ||
| </div> | ||
| <p className="text-sm text-gray-500 dark:text-gray-400"> | ||
| {connections.length} instance{connections.length !== 1 ? "s" : ""} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="flex items-center text-sm gap-2"> | ||
| {hasConnectedInstance && ( | ||
| <button | ||
| className="flex items-center gap-1 p-2 rounded-md text-tw-primary border border-tw-primary/70 | ||
| hover:bg-tw-primary hover:text-white" | ||
| onClick={handleOpenCluster} | ||
| > | ||
| <CircleChevronRight size={16} /> | ||
| Open Topology | ||
| </button> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| {isOpen && ( | ||
| <div className="border-t dark:border-tw-dark-border"> | ||
| <div className="p-2 space-y-1"> | ||
| {connections.map(({ connectionId, connection }) => ( | ||
| <ConnectionEntry | ||
| clusterId={clusterId} | ||
| connection={connection} | ||
| connectionId={connectionId} | ||
| hideOpenButton={true} | ||
| isNested={true} | ||
| key={connectionId} | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,14 +4,39 @@ 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<string, Array<{ connectionId: string; connection: ConnectionState }>> | ||
| standaloneConnections: Array<{ connectionId: string; connection: ConnectionState }> | ||
| }>( | ||
| (acc, [connectionId, connection]) => { | ||
| const clusterId = connection.connectionDetails.clusterId | ||
| if (clusterId) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe using nullish coalescing assignment operator can improve readbility here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adressed |
||
| if (!acc.clusterGroups[clusterId]) { | ||
| acc.clusterGroups[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 = () => | ||
| <button | ||
| className="bg-tw-primary text-white px-2 rounded text-sm font-light py-1 cursor-pointer" | ||
|
|
@@ -27,40 +52,63 @@ export function Connection() { | |
| <h1 className="text-xl font-bold flex items-center gap-2 text-gray-700 dark:text-white"> | ||
| <HousePlug /> Connections | ||
| </h1> | ||
| { R.isNotEmpty(connections) && connectButton() } | ||
| {R.isNotEmpty(connections) && connectButton()} | ||
| </div> | ||
| { | ||
| showConnectionForm && | ||
| <ConnectionForm onClose={() => setShowConnectionForm(false)} /> | ||
| } | ||
| { | ||
| showEditForm && // todo make this a separate URL /:id/edit; it's also unclear what edit means beside saving it in localStorage | ||
| <EditForm onClose={() => setShowEditForm(false)} /> | ||
| } | ||
| { | ||
| R.isEmpty(connections) ? | ||
| <div className=" bg-white dark:bg-tw-dark-primary dark:border-tw-dark-border flex-1 flex items-center justify-center flex-col gap-2"> | ||
| <span className="text-sm font-light text-gray-500 dark:text-white"> | ||
|
|
||
| {showConnectionForm && <ConnectionForm onClose={() => setShowConnectionForm(false)} />} | ||
| {showEditForm && <EditForm onClose={() => setShowEditForm(false)} />} | ||
|
|
||
| {R.isEmpty(connections) ? ( | ||
| <div className="flex-1 flex items-center justify-center flex-col gap-4"> | ||
| <div className="text-center"> | ||
| <h2 className="text-lg font-semibold text-gray-700 dark:text-white mb-2"> | ||
| You Have No Connections! | ||
| </span> | ||
| <p className="text-sm font-light text-gray-500 dark:text-white"> | ||
| Click "+ Add Connection" button to connect to a Valkey instance. | ||
| </h2> | ||
| <p className="text-sm text-gray-500 dark:text-gray-400 mb-6"> | ||
| Click "+ Add Connection" button to connect to a Valkey instance or cluster. | ||
| </p> | ||
| { connectButton() } | ||
| </div> : | ||
| <div className="border-t-1 mt-8 flex flex-col flex-1"> | ||
| <ConnectionEntryHeader /> | ||
| { | ||
| Object.entries(connections).map(([connectionId, connection]) => | ||
| <ConnectionEntry | ||
| clusterId={connection.connectionDetails.clusterId} | ||
| connection={connection} | ||
| connectionId={connectionId} | ||
| key={connectionId} | ||
| />) | ||
| } | ||
| {connectButton()} | ||
| </div> | ||
| } | ||
| </div> | ||
| ) : ( | ||
| <div className="flex-1 mt-8"> | ||
| {/* for clusters */} | ||
| {hasClusterGroups && ( | ||
| <div className="mb-8"> | ||
| <h2 className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3 px-1"> | ||
| Clusters | ||
| </h2> | ||
| <div> | ||
| {Object.entries(clusterGroups).map(([clusterId, clusterConnections]) => ( | ||
| <ClusterConnectionGroup | ||
| clusterId={clusterId} | ||
| connections={clusterConnections} | ||
| key={clusterId} | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
||
| {/* for standalone instances */} | ||
| {hasStandaloneConnections && ( | ||
| <div> | ||
| <h2 className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3 px-1"> | ||
| Instances | ||
| </h2> | ||
| <div> | ||
| {standaloneConnections.map(({ connectionId, connection }) => ( | ||
| <ConnectionEntry | ||
| connection={connection} | ||
| connectionId={connectionId} | ||
| key={connectionId} | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
won't this fail the linter because of the imports order? it should be below lucide-react
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This in not effecting the linter, but I still organized it in the new commit.