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
32 changes: 19 additions & 13 deletions apps/frontend/src/components/activity-view/ActivityView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useParams } from "react-router"
import { COMMANDLOG_TYPE } from "@common/src/constants"
import { truncateText } from "@common/src/truncate-text"
import { MONITOR_ACTION } from "@common/src/constants"
import { toNodeId } from "@common/src/connection-id.ts"
import { AppHeader } from "../ui/app-header"
import { TabGroup } from "../ui/tab-group"
import { ButtonGroup } from "../ui/button-group"
Expand All @@ -22,7 +23,7 @@ import {
hotKeysRequested, selectHotKeys, selectHotKeysStatus, selectHotKeysError,
selectHotKeysNodeErrors, selectHotKeysLastCollectedAt
} from "@/state/valkey-features/hotkeys/hotKeysSlice"
import { monitorRequested, selectMonitorRunning } from "@/state/valkey-features/monitor/monitorSlice"
import { monitorRequested, selectMonitorRunning, selectClusterMonitorRunning } from "@/state/valkey-features/monitor/monitorSlice"
import { selectConnectionDetails, selectClusterAlias } from "@/state/valkey-features/connection/connectionSelectors"
import { getKeyTypeRequested } from "@/state/valkey-features/keys/keyBrowserSlice"
import { selectKeys } from "@/state/valkey-features/keys/keyBrowserSelectors"
Expand All @@ -48,18 +49,23 @@ export const ActivityView = () => {
const [selectedKey, setSelectedKey] = useState<string | null>(null)
const [configOpen, setConfigOpen] = useState(false)

const commandLogsId = clusterId ?? id!
const commandLogsSlowData = useSelector((state: RootState) => selectCommandLogs(commandLogsId, COMMANDLOG_TYPE.SLOW)(state))
const commandLogsLargeRequestData = useSelector((state: RootState) => selectCommandLogs(commandLogsId, COMMANDLOG_TYPE.LARGE_REQUEST)(state))
const commandLogsLargeReplyData = useSelector((state: RootState) => selectCommandLogs(commandLogsId, COMMANDLOG_TYPE.LARGE_REPLY)(state))
const commandLogsNodeErrors = useSelector((state: RootState) => selectCommandLogsNodeErrors(commandLogsId)(state))
const hotKeysId = clusterId ?? id!
const hotKeysData = useSelector((state: RootState) => selectHotKeys(hotKeysId)(state))
const hotKeysStatus = useSelector((state: RootState) => selectHotKeysStatus(hotKeysId)(state))
const hotKeysErrorMessage = useSelector((state: RootState) => selectHotKeysError(hotKeysId)(state))
const hotKeysNodeErrors = useSelector((state: RootState) => selectHotKeysNodeErrors(hotKeysId)(state))
const hotKeysLastCollectedAt = useSelector((state: RootState) => selectHotKeysLastCollectedAt(hotKeysId)(state))
const monitorRunning = useSelector(selectMonitorRunning(id!))
// `targetId` keys node-level metrics state: `clusterId` for a cluster, else
// the db-less `nodeId`. (Connection-scoped state below still uses `id`, the
// db-suffixed connectionId.)
const nodeId = toNodeId(id!)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to just store nodeId in frontend state instead of stripping the db suffixed ID every time we need it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually are using targetId as the key in the frontend state for node level metrics state. This can be seen in the following functions in the code i.e. selectCommandLogs()(), selectHotKeys()()...

The reason for keeping the connectionId key state is that we still have things that need to be scoped by db such as the connection details, keys etc.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more so referring to storing nodeId in connection state, so components can read it directly instead of calling toNodeId every time. But this isn't a blocker if you don't think its worth the time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding nodeId in connection state means that if the connectionId gets changed, we have to ensure nodeId stays consistent, which is an additional thing to manage. toNodeId() is also cheap operation so there's no real performance benefit from caching it directly in connection state.

So in my mind it's not worth gaining that really small performance gain in exchange for an additional state we have to manage.

const targetId = clusterId ?? nodeId
const commandLogsSlowData = useSelector((state: RootState) => selectCommandLogs(targetId, COMMANDLOG_TYPE.SLOW)(state))
const commandLogsLargeRequestData = useSelector((state: RootState) => selectCommandLogs(targetId, COMMANDLOG_TYPE.LARGE_REQUEST)(state))
const commandLogsLargeReplyData = useSelector((state: RootState) => selectCommandLogs(targetId, COMMANDLOG_TYPE.LARGE_REPLY)(state))
const commandLogsNodeErrors = useSelector((state: RootState) => selectCommandLogsNodeErrors(targetId)(state))
const hotKeysData = useSelector((state: RootState) => selectHotKeys(targetId)(state))
const hotKeysStatus = useSelector((state: RootState) => selectHotKeysStatus(targetId)(state))
const hotKeysErrorMessage = useSelector((state: RootState) => selectHotKeysError(targetId)(state))
const hotKeysNodeErrors = useSelector((state: RootState) => selectHotKeysNodeErrors(targetId)(state))
const hotKeysLastCollectedAt = useSelector((state: RootState) => selectHotKeysLastCollectedAt(targetId)(state))
const monitorRunning = useSelector((state: RootState) =>
clusterId ? selectClusterMonitorRunning(clusterId)(state) : selectMonitorRunning(nodeId)(state),
)
const connectionDetails = useSelector((state: RootState) => selectConnectionDetails(id!)(state))
const clusterAlias = useSelector(selectClusterAlias(id!))
const useHotSlots = connectionDetails?.keyEvictionPolicy?.includes("lfu") && connectionDetails?.clusterSlotStatsEnabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type LogType = "slow" | "large-request" | "large-reply"
interface CommandLogTableProps {
data: LogGroup[] | null
logType: LogType
nodeErrors?: { connectionId: string; error: string }[]
nodeErrors?: { nodeId: string; error: string }[]
isCluster?: boolean
}

Expand Down Expand Up @@ -102,10 +102,10 @@ export function CommandLogTable({ data, logType, nodeErrors, isCluster }: Comman
{nodeErrorsExpanded && (
<ul className="absolute z-50 left-0 mt-0.5 right-0 p-3 max-h-40 overflow-y-auto space-y-0.5
rounded-md border bg-accent shadow-sm">
{nodeErrors.map(({ connectionId, error }) => (
<li key={connectionId}>
{nodeErrors.map(({ nodeId, error }) => (
<li key={nodeId}>
<Typography variant="bodySm">
<span className="font-mono">{connectionId}</span>: {error}
<span className="font-mono">{nodeId}</span>: {error}
</Typography>
</li>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Alert, AlertDescription } from "../../ui/alert"
import { Typography } from "../../ui/typography"

interface NodeErrorsBannerProps {
nodeErrors: { connectionId: string; error: string }[]
nodeErrors: { nodeId: string; error: string }[]
}

export function NodeErrorsBanner({ nodeErrors }: NodeErrorsBannerProps) {
Expand Down Expand Up @@ -44,10 +44,10 @@ export function NodeErrorsBanner({ nodeErrors }: NodeErrorsBannerProps) {
{expanded && (
<ul className="absolute z-50 left-0 right-0 mt-0.5 p-3 max-h-40 overflow-y-auto space-y-0.5
rounded-md border bg-accent shadow-sm">
{nodeErrors.map(({ connectionId, error }) => (
<li key={connectionId}>
{nodeErrors.map(({ nodeId, error }) => (
<li key={nodeId}>
<Typography variant="bodySm">
<span className="font-mono">{connectionId}</span>: {error}
<span className="font-mono">{nodeId}</span>: {error}
</Typography>
</li>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import { useParams } from "react-router"
import { AlertTriangle } from "lucide-react"
import { TooltipProvider } from "@radix-ui/react-tooltip"
import { MONITOR_ACTION } from "@common/src/constants"
import { toNodeId } from "@common/src/connection-id.ts"
import { ChartModal } from "../../ui/chart-modal"
import { Button } from "../../ui/button"
import { Input } from "../../ui/input"
import { Typography } from "../../ui/typography"
import { TooltipIcon } from "../../ui/tooltip-icon"
import type { RootState } from "@/store"
import { useAppDispatch } from "@/hooks/hooks"
import { selectConfig } from "@/state/valkey-features/config/configSlice"
import { saveMonitorSettingsRequested, selectMonitorRunning } from "@/state/valkey-features/monitor/monitorSlice"
import { saveMonitorSettingsRequested, selectMonitorRunning, selectClusterMonitorRunning } from "@/state/valkey-features/monitor/monitorSlice"

interface HotKeysConfigModalProps {
open: boolean
Expand All @@ -21,8 +23,11 @@ interface HotKeysConfigModalProps {
export function HotKeysParamsModal({ open, onClose }: HotKeysConfigModalProps) {
const { id, clusterId } = useParams()
const dispatch = useAppDispatch()
const config = useSelector(selectConfig(id!))
const monitorRunning = useSelector(selectMonitorRunning(id!))
// Config and monitor state are cluster-keyed for clusters, node-keyed otherwise.
const config = useSelector(selectConfig(clusterId ?? toNodeId(id!)))
const monitorRunning = useSelector((state: RootState) =>
clusterId ? selectClusterMonitorRunning(clusterId)(state) : selectMonitorRunning(toNodeId(id!))(state),
)

const [monitorDuration, setMonitorDuration] = useState(config?.monitoring?.monitoringDuration ?? 10000)
const [monitorInterval, setMonitorInterval] = useState(config?.monitoring?.monitoringInterval ?? 10000)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface HotKeysProps {
errorMessage: string | null
status?: string
monitorRunning?: boolean
nodeErrors?: { connectionId: string; error: string }[]
nodeErrors?: { nodeId: string; error: string }[]
isCluster?: boolean
onKeyClick?: (keyName: string) => void
onStartMonitoring?: () => void
Expand Down
45 changes: 21 additions & 24 deletions apps/frontend/src/components/ui/monitor-warning-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { Button } from "./button"
import { Typography } from "./typography"
import { useAppDispatch } from "@/hooks/hooks"
import { monitorRequested, selectRunningMonitorConnections } from "@/state/valkey-features/monitor/monitorSlice"
import { selectAllClusters } from "@/state/valkey-features/cluster/clusterSelectors"

interface MonitoringConfig {
monitoringDuration: number
Expand All @@ -18,7 +17,6 @@ interface MonitoringConfig {
export function MonitorWarningBanner() {
const dispatch = useAppDispatch()
const runningConnections = useSelector(selectRunningMonitorConnections)
const clusters = useSelector(selectAllClusters)
const configState = useSelector((state: unknown) =>
R.path<Record<string, { monitoring?: MonitoringConfig }>>([VALKEY.CONFIG.name], state) ?? {},
)
Expand All @@ -32,28 +30,27 @@ export function MonitorWarningBanner() {
}, [runningConnections.length])

const { clusterGroups, standaloneConnections } = useMemo(() => {
const groups: Record<string, { connectionId: string; startedAt: number | null }[]> = {}
const standalone: { connectionId: string; startedAt: number | null }[] = []
const groups: Record<string, { nodeId: string; startedAt: number | null }[]> = {}
const standalone: { nodeId: string; startedAt: number | null }[] = []

for (const [clusterId, cluster] of Object.entries(clusters)) {
const clusterNodeIds = new Set(Object.keys(cluster.clusterNodes ?? {}))
const matching = runningConnections.filter((c) => clusterNodeIds.has(c.connectionId))
if (matching.length > 0) groups[clusterId] = matching
}

const grouped = new Set(Object.values(groups).flat().map((c) => c.connectionId))
// Monitor entries are grouped by clusterId on the cluster path.
// Standalone entries have no clusterId.
for (const conn of runningConnections) {
if (!grouped.has(conn.connectionId)) standalone.push(conn)
if (conn.clusterId) {
(groups[conn.clusterId] ??= []).push(conn)
} else {
standalone.push(conn)
}
Comment thread
ravjotbrar marked this conversation as resolved.
}

return { clusterGroups: groups, standaloneConnections: standalone }
}, [runningConnections, clusters])
}, [runningConnections])

if (runningConnections.length === 0) return null

const handleStop = (connectionId: string, cId?: string) => {
const handleStop = (nodeId: string, cId?: string) => {
dispatch(monitorRequested({
connectionId,
connectionId: nodeId,
clusterId: cId,
monitorAction: MONITOR_ACTION.STOP,
}))
Expand Down Expand Up @@ -91,7 +88,7 @@ export function MonitorWarningBanner() {
<div className="flex items-center justify-between gap-2">
<span className="font-mono text-xs truncate flex-1">{cId}</span>
<Button
onClick={() => handleStop(nodes[0].connectionId, cId)}
onClick={() => handleStop(nodes[0].nodeId, cId)}
size={"sm"}
variant={"destructive"}
>
Expand All @@ -104,20 +101,20 @@ export function MonitorWarningBanner() {
</span>
)}
<span className="text-xs text-gray-400 flex items-center">
Duration: {milliSecondsToSeconds(configState[nodes[0].connectionId]?.monitoring?.monitoringDuration ?? 10000)} <Dot />
Interval: {milliSecondsToSeconds(configState[nodes[0].connectionId]?.monitoring?.monitoringInterval ?? 10000)} <Dot />
Duration: {milliSecondsToSeconds(configState[cId]?.monitoring?.monitoringDuration ?? 10000)} <Dot />
Interval: {milliSecondsToSeconds(configState[cId]?.monitoring?.monitoringInterval ?? 10000)} <Dot />
Nodes: {nodes.length}
</span>
</div>
))}

{/* Standalone — with its own button */}
{standaloneConnections.map(({ connectionId, startedAt }) => (
<div className="border-b p-2 flex flex-col last:border-b-0" key={connectionId}>
{standaloneConnections.map(({ nodeId, startedAt }) => (
<div className="border-b p-2 flex flex-col last:border-b-0" key={nodeId}>
<div className="flex items-center justify-between gap-2">
<span className="font-mono text-xs truncate flex-1">{connectionId}</span>
<span className="font-mono text-xs truncate flex-1">{nodeId}</span>
<Button
onClick={() => handleStop(connectionId)}
onClick={() => handleStop(nodeId)}
size={"sm"}
variant={"destructive"}
>
Expand All @@ -130,8 +127,8 @@ export function MonitorWarningBanner() {
</span>
)}
<span className="text-xs text-gray-400 flex items-center">
Duration: {milliSecondsToSeconds(configState[connectionId]?.monitoring?.monitoringDuration ?? 10000)} <Dot />
Interval: {milliSecondsToSeconds(configState[connectionId]?.monitoring?.monitoringInterval ?? 10000)}
Duration: {milliSecondsToSeconds(configState[nodeId]?.monitoring?.monitoringDuration ?? 10000)} <Dot />
Interval: {milliSecondsToSeconds(configState[nodeId]?.monitoring?.monitoringInterval ?? 10000)}
</span>
</div>
))}
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/state/epics/valkeyEpics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ export const setDataEpic = (store: Store) =>
connectionDetails: { clusterId },
} = action.payload as unknown as { connectionId:string, connectionDetails: { clusterId?: string } }

store.dispatch(setConfig( action.payload))
store.dispatch(setConfig(action.payload))

if (action.type === clusterConnectFulfilled.type) {
socket.next({ type: setClusterData.type, payload: { clusterId, connectionId } })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { describe, it, expect } from "vitest"
import { COMMANDLOG_TYPE } from "@common/src/constants.ts"
import commandLogsReducer, {
commandLogsRequested,
commandLogsFulfilled,
commandLogsError
} from "./commandLogsSlice"

describe("commandLogsSlice", () => {
const initialState = {}

describe("commandLogsRequested", () => {
it("keys the standalone pending entry by the db-less nodeId", () => {
const state = commandLogsReducer(
initialState,
commandLogsRequested({ connectionId: "host-6379-db0" }),
)

expect(state["host-6379"]).toBeDefined()
expect(state["host-6379-db0"]).toBeUndefined()
expect(state["host-6379"].loading).toBe(true)
})

it("collapses two connections to the same node on different dbs into one entry", () => {
const afterDb0 = commandLogsReducer(
initialState,
commandLogsRequested({ connectionId: "host-6379-db0" }),
)
const afterDb1 = commandLogsReducer(
afterDb0,
commandLogsRequested({ connectionId: "host-6379-db1" }),
)

expect(Object.keys(afterDb1)).toEqual(["host-6379"])
})

it("keys the cluster pending entry by clusterId", () => {
const state = commandLogsReducer(
initialState,
commandLogsRequested({ connectionId: "node-1-db0", clusterId: "cluster-1" }),
)

expect(state["cluster-1"]).toBeDefined()
expect(state["node-1-db0"]).toBeUndefined()
})
})

describe("commandLogsFulfilled", () => {
it("keys a standalone reply by nodeId", () => {
const rows = [{ ts: 1, metric: "slow", values: [] }]
const state = commandLogsReducer(
initialState,
commandLogsFulfilled({
nodeId: "host-6379",
commandLogType: COMMANDLOG_TYPE.SLOW,
parsedResponse: { rows, count: 10 },
}),
)

expect(state["host-6379"]).toBeDefined()
expect(state["host-6379"].logs[COMMANDLOG_TYPE.SLOW]).toEqual(rows)
expect(state["host-6379"].count).toBe(10)
expect(state["host-6379"].loading).toBe(false)
})

it("keys a cluster reply by clusterId and keeps nodeErrors db-less", () => {
const state = commandLogsReducer(
initialState,
commandLogsFulfilled({
clusterId: "cluster-1",
commandLogType: COMMANDLOG_TYPE.SLOW,
parsedResponse: { rows: [], count: 0 },
nodeErrors: [{ nodeId: "node-1", error: "down" }],
}),
)

expect(state["cluster-1"]).toBeDefined()
expect(state["cluster-1"].nodeErrors).toEqual([{ nodeId: "node-1", error: "down" }])
})
})

describe("commandLogsError", () => {
it("keys a standalone error by nodeId", () => {
const seeded = commandLogsReducer(
initialState,
commandLogsRequested({ connectionId: "host-6379-db0" }),
)
const state = commandLogsReducer(
seeded,
commandLogsError({ nodeId: "host-6379", error: { message: "boom" } }),
)

expect(state["host-6379"].error).toEqual({ message: "boom" })
expect(state["host-6379"].loading).toBe(false)
})

it("keys a cluster error by clusterId", () => {
const seeded = commandLogsReducer(
initialState,
commandLogsRequested({ connectionId: "node-1-db0", clusterId: "cluster-1" }),
)
const state = commandLogsReducer(
seeded,
commandLogsError({ clusterId: "cluster-1", error: { message: "boom" } }),
)

expect(state["cluster-1"].error).toEqual({ message: "boom" })
})
})
})
Loading
Loading