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
122 changes: 89 additions & 33 deletions apps/frontend/src/components/activity-view/ActivityView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { TabGroup } from "../ui/tab-group"
import { ButtonGroup } from "../ui/button-group"
import { HotKeys } from "./hotkeys/hot-keys"
import { HotKeysParamsModal } from "./hotkeys/hot-keys-params-modal"
import { BigKeys } from "./bigkeys/big-keys"
import { CommandLogTable } from "./command-log-table"
import KeyDetails from "../key-browser/key-details/key-details"
import RouteContainer from "../ui/route-container"
Expand All @@ -23,17 +24,21 @@ import {
hotKeysRequested, selectHotKeys, selectHotKeysStatus, selectHotKeysError,
selectHotKeysNodeErrors, selectHotKeysLastCollectedAt
} from "@/state/valkey-features/hotkeys/hotKeysSlice"
import {
monitorRequested,
selectMonitorRunning,
selectClusterMonitorRunning,
selectMonitorError
import {
bigKeysRequested, selectBigKeys, selectBigKeysStatus, selectBigKeysError,
selectBigKeysNodeErrors, selectBigKeysScanned, selectBigKeysTotalKeys
} from "@/state/valkey-features/bigkeys/bigKeysSlice"
import {
monitorRequested,
selectMonitorRunning,
selectClusterMonitorRunning,
selectMonitorError
} 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"

type TabType = "hot-keys" | "command-logs"
type TabType = "hot-keys" | "big-keys" | "command-logs"
type CommandLogSubTab = "slow" | "large-request" | "large-reply"

interface KeyInfo {
Expand Down Expand Up @@ -68,6 +73,12 @@ export const ActivityView = () => {
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 bigKeysData = useSelector((state: RootState) => selectBigKeys(targetId)(state))
const bigKeysStatus = useSelector((state: RootState) => selectBigKeysStatus(targetId)(state))
const bigKeysErrorMessage = useSelector((state: RootState) => selectBigKeysError(targetId)(state))
const bigKeysNodeErrors = useSelector((state: RootState) => selectBigKeysNodeErrors(targetId)(state))
const bigKeysScanned = useSelector((state: RootState) => selectBigKeysScanned(targetId)(state))
const bigKeysTotalKeys = useSelector((state: RootState) => selectBigKeysTotalKeys(targetId)(state))
const monitorRunning = useSelector((state: RootState) =>
clusterId ? selectClusterMonitorRunning(clusterId)(state) : selectMonitorRunning(nodeId)(state),
)
Expand Down Expand Up @@ -107,6 +118,19 @@ export const ActivityView = () => {
}
}

// Big keys are scanned on demand - they get fetched when the tab is opened
useEffect(() => {
if (id && activeTab === "big-keys" && bigKeysStatus === undefined) {
dispatch(bigKeysRequested({ connectionId: id, clusterId }))
}
}, [activeTab, bigKeysStatus, id, clusterId, dispatch])

const refreshBigKeys = () => {
if (id) {
dispatch(bigKeysRequested({ connectionId: id, clusterId }))
}
}

const getCurrentCommandLogData = () => {
switch (commandLogSubTab) {
case "slow":
Expand All @@ -133,8 +157,12 @@ export const ActivityView = () => {
? keys.find((k) => k.name === selectedKey) ?? null
: null

// Big keys can exceed the readable size limit, so details are hot-keys only.
const showKeyDetails = activeTab === "hot-keys" && !!selectedKey

const tabs = [
{ id: "hot-keys" as TabType, label: "Hot Keys" },
{ id: "big-keys" as TabType, label: "Big Keys" },
{ id: "command-logs" as TabType, label: "Command Logs" },
]

Expand All @@ -150,7 +178,7 @@ export const ActivityView = () => {
<AppHeader
description={
<>
Monitor Hot Keys and Command Logs of{" "}
Monitor Hot Keys, Big Keys and Command Logs of{" "}
{clusterId ? (
<>
cluster {" "} <span className="font-semibold text-primary">{truncateText(clusterAlias || clusterId!)}</span>
Expand Down Expand Up @@ -186,6 +214,24 @@ export const ActivityView = () => {
</div>
)}

{/* Big Keys Refresh */}
{activeTab === "big-keys" && (
<div className="flex items-center gap-3">
{bigKeysScanned !== null && bigKeysTotalKeys !== null && (
<Typography variant="bodyXs">
Scanned {bigKeysScanned.toLocaleString()} of {bigKeysTotalKeys.toLocaleString()} keys
</Typography>
)}
<Button
onClick={refreshBigKeys}
size={"sm"}
variant={"outline"}
>
Refresh <RefreshCcw className="hover:text-primary" size={15} />
</Button>
</div>
)}

{/* Command Log Sub-tabs and Refresh */}
{activeTab === "command-logs" && (
<div className="flex items-center gap-3">
Expand All @@ -208,27 +254,46 @@ export const ActivityView = () => {
</div>

{/* Tab Content */}
{activeTab === "hot-keys" ? (
{activeTab === "command-logs" ? (
<div className="flex-1 h-full overflow-hidden border border-input rounded-md shadow-xs">
<CommandLogTable
data={getCurrentCommandLogData()}
isCluster={!!clusterId}
logType={commandLogSubTab}
nodeErrors={commandLogsNodeErrors}
/>
</div>
) : (
<div className="flex flex-1 h-full overflow-hidden gap-2">
{/* Hot Keys List */}
<div className={`${selectedKey ? "w-2/3" : "w-full"} h-full min-w-0 overflow-hidden`}>
{/* Keys List (hot keys or big keys) */}
<div className={`${showKeyDetails ? "w-2/3" : "w-full"} h-full min-w-0 overflow-hidden`}>
<div className="h-full border border-input rounded-md shadow-xs overflow-hidden">
<HotKeys
data={hotKeysData}
errorMessage={hotKeysErrorMessage as string | null}
isCluster={!!clusterId}
monitorError={monitorError}
monitorRunning={monitorRunning}
nodeErrors={hotKeysNodeErrors}
onKeyClick={handleKeyClick}
onStartMonitoring={useHotSlots ? undefined : () => setConfigOpen(true)}
selectedKey={selectedKey}
status={hotKeysStatus}
/>
{activeTab === "hot-keys" ? (
<HotKeys
data={hotKeysData}
errorMessage={hotKeysErrorMessage as string | null}
isCluster={!!clusterId}
monitorError={monitorError}
monitorRunning={monitorRunning}
nodeErrors={hotKeysNodeErrors}
onKeyClick={handleKeyClick}
onStartMonitoring={useHotSlots ? undefined : () => setConfigOpen(true)}
selectedKey={selectedKey}
status={hotKeysStatus}
/>
) : (
<BigKeys
data={bigKeysData}
errorMessage={bigKeysErrorMessage as string | null}
isCluster={!!clusterId}
nodeErrors={bigKeysNodeErrors}
status={bigKeysStatus as string | undefined}
/>
)}
</div>
</div>
{/* Key Details Panel */}
{selectedKey && (
{/* Key Details Panel (hot keys only; big keys can exceed the readable size limit) */}
{showKeyDetails && (
<div className="w-1/3 h-full min-w-0">
<KeyDetails
connectionId={id!}
Expand All @@ -240,15 +305,6 @@ export const ActivityView = () => {
</div>
)}
</div>
) : (
<div className="flex-1 h-full overflow-hidden border border-input rounded-md shadow-xs">
<CommandLogTable
data={getCurrentCommandLogData()}
isCluster={!!clusterId}
logType={commandLogSubTab}
nodeErrors={commandLogsNodeErrors}
/>
</div>
)}
</RouteContainer>

Expand Down
126 changes: 126 additions & 0 deletions apps/frontend/src/components/activity-view/bigkeys/big-keys-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Copy, KeyRound } from "lucide-react"
import { toast } from "sonner"
import { convertTTL } from "@common/src/ttl-conversion"
import { formatBytes } from "@common/src/bytes-conversion"
import { truncateText } from "@common/src/truncate-text"
import { TableContainer } from "../../ui/table-container"
import { SortableTableHeader, StaticTableHeader, type SortOrder } from "../../ui/sortable-table-header"
import { Typography } from "../../ui/typography"
import { CustomTooltip } from "../../ui/tooltip"
import type { BigKey } from "@/state/valkey-features/bigkeys/bigKeysSlice"
import { copyToClipboard } from "@/lib/utils"

interface BigKeyRowProps {
entry: BigKey
}

function BigKeyRow({ entry }: BigKeyRowProps) {
const { key, sizeBytes, type, ttl, nodeId } = entry

const handleCopy = () => {
copyToClipboard(key)
toast.success("Key name copied!")
}

return (
<tr className="group border-b dark:border-tw-dark-border transition-all duration-200 hover:bg-gray-50 dark:hover:bg-neutral-800/50">
<td className="px-4 py-3 w-1/3">
<div className="flex items-center gap-2">
<Typography className="truncate" variant="code">{key}</Typography>
<button
aria-label="Copy key name"
className="p-1 rounded text-primary hover:bg-primary/20"
onClick={handleCopy}
>
<Copy size={14} />
</button>
</div>
</td>
<td className="px-4 py-3 w-1/6 text-center">
<Typography variant="code">{type}</Typography>
</td>
<td className="px-4 py-3 w-1/6 text-center">
<Typography variant="bodySm">{formatBytes(sizeBytes)}</Typography>
</td>
<td className="px-4 py-3 w-1/6 text-center">
<Typography variant="bodySm">{convertTTL(ttl)}</Typography>
</td>
<td className="px-4 py-3 w-1/6 text-center">
<CustomTooltip content={nodeId ?? "-"}>
<Typography variant="code">{truncateText(nodeId ?? "—")}</Typography>
</CustomTooltip>
</td>
</tr>
)
}

interface NoMatchRowProps {
searchQuery: string
selectedNode: string
}

function NoMatchRow({ searchQuery, selectedNode }: NoMatchRowProps) {
return (
<tr>
<td className="px-4 py-8 text-center" colSpan={5}>
<Typography variant="bodySm">
No keys match
{searchQuery && (
<Typography className="text-primary ml-1" variant="code">{searchQuery}</Typography>
)}
{selectedNode !== "all" && (
<span> on node <Typography className="text-primary" variant="code">{selectedNode}</Typography></span>
)}
</Typography>
</td>
</tr>
)
}

interface BigKeysTableProps {
rows: BigKey[]
sortOrder: SortOrder
onToggleSort: () => void
searchQuery: string
selectedNode: string
}

export function BigKeysTable({
rows, sortOrder, onToggleSort, searchQuery, selectedNode,
}: BigKeysTableProps) {
return (
<TableContainer
header={
<>
<StaticTableHeader
icon={<KeyRound className="text-primary" size={16} />}
label="Key Name"
width="w-1/3"
/>
<StaticTableHeader className="text-center" label="Type" width="w-1/6" />
<SortableTableHeader
active={true}
className="text-center"
label="Size"
onClick={onToggleSort}
sortOrder={sortOrder}
width="w-1/6"
/>
<StaticTableHeader className="text-center" label="TTL" width="w-1/6" />
<StaticTableHeader className="text-center" label="Node" width="w-1/6" />
</>
}
>
{rows.length === 0 ? (
<NoMatchRow searchQuery={searchQuery} selectedNode={selectedNode} />
) : (
rows.map((entry, index) => (
<BigKeyRow
entry={entry}
key={`${entry.key}-${index}`}
/>
))
)}
</TableContainer>
)
}
Loading
Loading