Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 47 additions & 20 deletions apps/frontend/src/components/ui/app-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useNavigate, useParams } from "react-router"
import { useSelector } from "react-redux"
import { useState, useRef, useEffect, type ReactNode } from "react"
import { CircleChevronDown, CircleChevronUp, Dot, CornerDownRight } from "lucide-react"
import { CONNECTED } from "@common/src/constants.ts"
import type { RootState } from "@/store.ts"
import { selectConnectionDetails } from "@/state/valkey-features/connection/connectionSelectors.ts"
import { selectCluster } from "@/state/valkey-features/cluster/clusterSelectors"
import { cn } from "@/lib/utils.ts"
Expand All @@ -21,6 +23,15 @@ function AppHeader({ title, icon, className }: AppHeaderProps) {
const clusterData = useSelector(selectCluster(clusterId!))
const ToggleIcon = isOpen ? CircleChevronUp : CircleChevronDown

const connectionStatus = useSelector((state: RootState) =>
state.valkeyConnection?.connections?.[id!]?.status,
)
const isConnected = connectionStatus === CONNECTED

const allConnections = useSelector((state: RootState) =>
state.valkeyConnection?.connections,
)

const handleNavigate = (port: number) => {
navigate(`/${clusterId}/localhost-${port}/dashboard`)
setIsOpen(false)
Expand Down Expand Up @@ -65,32 +76,48 @@ function AppHeader({ title, icon, className }: AppHeaderProps) {
<div>
<div className="h-5 w-50 px-2 py-4 border-tw-primary border rounded flex items-center gap-2 justify-between">
<div className="flex flex-col gap-1">
<span className="font-light text-sm text-tw-primary flex items-center"><Dot className="text-green-500" size={45} />{id}</span>
<span className="font-light text-sm text-tw-primary flex items-center">
<Dot className={isConnected ? "text-green-500" : "text-gray-400"} size={45} />
{id}
</span>
</div>
<button onClick={() => setIsOpen(!isOpen)}>
<ToggleIcon className="text-tw-primary cursor-pointer hover:text-tw-primary/80" size={18} />
<button disabled={!isConnected} onClick={() => isConnected && setIsOpen(!isOpen)}>
<ToggleIcon
className={isConnected
? "text-tw-primary cursor-pointer hover:text-tw-primary/80"
: "text-gray-400 cursor-not-allowed"
}
size={18}
/>
</button>
</div>
{isOpen && (
<div className="p-4 w-50 py-3 border bg-gray-50 dark:bg-gray-800 text-sm dark:border-tw-dark-border
<div className="p-4 w-50 py-3 border bg-gray-50 dark:bg-gray-800 text-sm dark:border-tw-dark-border
rounded z-10 absolute top-10 right-0" ref={dropdownRef}>
<ul className="space-y-2">
{Object.entries(clusterData.clusterNodes).map(([primaryKey, primary]) => (
<li className="flex flex-col gap-1" key={primaryKey}>
<button className="font-normal flex items-center cursor-pointer hover:bg-tw-primary/20"
onClick={() => handleNavigate(primary.port)}>
<Dot className="text-green-500" size={45} />
{primaryKey}</button>
{primary.replicas?.map((replica) => (
<div className="flex items-center ml-4">
<CornerDownRight className="text-tw-dark-border" size={20} />
<button className="font-normal flex items-center text-xs">
<Dot className="text-tw-primary" size={24} />{replica.host}:{replica.port}
</button>
</div>
))}
</li>
))}
{Object.entries(clusterData.clusterNodes).map(([primaryKey, primary]) => {
const nodeConnectionId = `localhost-${primary.port}`
const nodeIsConnected = allConnections?.[nodeConnectionId]?.status === CONNECTED

return (
<li className="flex flex-col gap-1" key={primaryKey}>
<button className="font-normal flex items-center cursor-pointer hover:bg-tw-primary/20"
disabled={!nodeIsConnected}
onClick={() => handleNavigate(primary.port)}>
<Dot className={nodeIsConnected ? "text-green-500" : "text-gray-400"} size={45} />
{primaryKey}
</button>
{primary.replicas?.map((replica) => (
<div className="flex items-center ml-4" key={replica.id}>
<CornerDownRight className="text-tw-dark-border" size={20} />
<button className="font-normal flex items-center text-xs">
<Dot className="text-tw-primary" size={24} />{replica.host}:{replica.port}
</button>
</div>
))}
</li>
)
})}
</ul>
</div>
)}
Expand Down
62 changes: 44 additions & 18 deletions apps/frontend/src/components/ui/cluster-node.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { Dot, LayoutDashboard, Terminal } from "lucide-react"
import { Dot, LayoutDashboard, Terminal, PowerIcon } from "lucide-react"
import { useNavigate } from "react-router"
import { useSelector } from "react-redux"
import { CONNECTED } from "@common/src/constants.ts"
import { TooltipProvider } from "@radix-ui/react-tooltip"
import { Card } from "./card"
import { CustomTooltip } from "./custom-tooltip"
import type { RootState } from "@/store.ts"
import { connectPending } from "@/state/valkey-features/connection/connectionSlice.ts"
import { useAppDispatch } from "@/hooks/hooks"

interface ReplicaNode {
id: string
Expand Down Expand Up @@ -36,6 +43,14 @@ interface ClusterNodeProps {

export default function ClusterNode({ primaryKey, primary, primaryData, allNodeData, clusterId }: ClusterNodeProps) {
const navigate = useNavigate()
const dispatch = useAppDispatch()

// to check if cluster node is connected
const connectionId = `localhost-${primary.port}`
const connectionStatus = useSelector((state: RootState) =>
state.valkeyConnection?.connections?.[connectionId]?.status,
)
const isConnected = connectionStatus === CONNECTED

const formatRole = (role: string | null) => {
if (!role) return "UNKNOWN"
Expand All @@ -45,12 +60,31 @@ export default function ClusterNode({ primaryKey, primary, primaryData, allNodeD
return role.toUpperCase()
}

const handleNodeConnect = () => {
if (!isConnected) {
dispatch(connectPending({
connectionId,
host: primary.host,
port: primary.port.toString(),
}))
}
}

return (
<Card className="dark:bg-gray-800">
{/* for primary */}
<div className="flex items-center">
<span className="font-bold">{formatRole(primaryData?.role)}</span> <Dot className="text-green-500" size={34} />
</div>
<TooltipProvider>
<div className="flex items-center justify-between">
<span className="font-bold">{formatRole(primaryData?.role)}</span>
<CustomTooltip content={`${isConnected ? "Connected" : "Not Connected"}`}>
<PowerIcon
className={`${isConnected ? "text-green-500 bg-green-100" : "text-gray-400 cursor-pointer bg-gray-100 hover:text-gray-600"} rounded-full p-0.5`}
onClick={handleNodeConnect}
size={18}
/>
</CustomTooltip>
</div>
</TooltipProvider>
<div className="flex flex-col text-xs text-tw-dark-border"><span>{primaryData?.server_name}</span><span>{primaryKey}</span></div>
<div className="text-xs space-y-1.5">
<div className="flex justify-between">
Expand Down Expand Up @@ -87,28 +121,20 @@ export default function ClusterNode({ primaryKey, primary, primaryData, allNodeD
</div>
<Dot className="text-tw-primary" size={30} />
</div>
{/* replica buttons */}
<div className="mt-2 flex gap-2">
<button className="flex items-center gap-1.5 border dark:border-tw-dark-border px-2 py-0.5
rounded cursor-pointer hover:bg-tw-primary hover:text-white"
onClick={() => { navigate(`/${clusterId}/localhost-${replica.port}/dashboard`) }}><LayoutDashboard size={12} />
Dashboard</button>
<button className="flex items-center gap-1.5 border dark:border-tw-dark-border px-2 py-0.5
rounded cursor-pointer hover:bg-tw-primary hover:text-white"
onClick={() => { navigate(`/${clusterId}/localhost-${replica.port}/sendcommand`) }}><Terminal size={12} /> Command</button>
</div>
</div>
))}
</div>
)}

{/* primary buttons */}
<div className="mt-2 flex gap-2 text-xs items-center justify-center">
<button className="w-1/2 flex items-center justify-center gap-1.5 border px-2 py-1 rounded cursor-pointer
hover:bg-tw-primary hover:text-white"
<button className={`w-1/2 flex items-center justify-center gap-1.5 border px-2 py-1 rounded
${isConnected ? "cursor-pointer hover:bg-tw-primary hover:text-white" : "cursor-not-allowed opacity-50"}`}
disabled={!isConnected}
onClick={() => { navigate(`/${clusterId}/localhost-${primary.port}/dashboard`) }}><LayoutDashboard size={12} /> Dashboard</button>
<button className="w-1/2 flex items-center justify-center gap-1.5 border px-2 py-1 rounded cursor-pointer
hover:bg-tw-primary hover:text-white"
<button className={`w-1/2 flex items-center justify-center gap-1.5 border px-2 py-1 rounded
${isConnected ? "cursor-pointer hover:bg-tw-primary hover:text-white" : "cursor-not-allowed opacity-50"}`}
disabled={!isConnected}
onClick={() => { navigate(`/${clusterId}/localhost-${primary.port}/sendcommand`) }}><Terminal size={12} /> Command</button>
</div>
</Card>
Expand Down
Loading