import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Table, Input, Button, Space, Tag, Tree, Spin, message, Modal, Form, InputNumber, Popconfirm, Tooltip, Radio } from 'antd'; import { ReloadOutlined, DeleteOutlined, PlusOutlined, EditOutlined, SearchOutlined, ClockCircleOutlined, CopyOutlined, FolderOpenOutlined, KeyOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import { RedisKeyInfo, RedisValue } from '../types'; import Editor from '@monaco-editor/react'; import type { DataNode } from 'antd/es/tree'; const { Search } = Input; const KEY_GROUP_DELIMITER = ':'; const EMPTY_SEGMENT_LABEL = '(empty)'; interface RedisViewerProps { connectionId: string; redisDB: number; } // 尝试多种方式解码二进制数据 const tryDecodeValue = (value: string): { displayValue: string; encoding: string; needsHex: boolean } => { if (!value || value.length === 0) { return { displayValue: '', encoding: 'UTF-8', needsHex: false }; } // 统计字节分布 let nullCount = 0; let printableCount = 0; let highByteCount = 0; const sampleSize = Math.min(value.length, 200); for (let i = 0; i < sampleSize; i++) { const code = value.charCodeAt(i); if (code === 0) { nullCount++; } else if (code >= 32 && code < 127) { printableCount++; } else if (code >= 128) { highByteCount++; } } // 如果超过30%是null字节,很可能是二进制数据,显示十六进制 if (nullCount / sampleSize > 0.3) { return { displayValue: toHexDisplay(value), encoding: 'HEX', needsHex: true }; } // 如果超过70%是可打印ASCII字符,直接显示 if (printableCount / sampleSize > 0.7) { return { displayValue: value, encoding: 'UTF-8', needsHex: false }; } // 尝试UTF-8解码 if (highByteCount > 0) { try { const bytes = new Uint8Array(value.length); for (let i = 0; i < value.length; i++) { bytes[i] = value.charCodeAt(i) & 0xFF; } const decoded = new TextDecoder('utf-8', { fatal: false }).decode(bytes); // 检查解码质量 let validChars = 0; let replacementChars = 0; let controlChars = 0; for (let i = 0; i < Math.min(decoded.length, 200); i++) { const code = decoded.charCodeAt(i); if (code === 0xFFFD) { replacementChars++; } else if (code < 32 && code !== 9 && code !== 10 && code !== 13) { controlChars++; } else if ((code >= 32 && code < 127) || (code >= 0x4E00 && code <= 0x9FFF) || (code >= 0x3000 && code <= 0x303F)) { // ASCII可打印字符、中文字符、中文标点 validChars++; } } const totalChecked = Math.min(decoded.length, 200); // 如果替换字符超过10%或控制字符超过20%,说明不是有效的UTF-8文本 if (replacementChars / totalChecked > 0.1 || controlChars / totalChecked > 0.2) { return { displayValue: toHexDisplay(value), encoding: 'HEX', needsHex: true }; } // 如果有效字符超过50%,使用UTF-8解码 if (validChars / totalChecked > 0.5) { return { displayValue: decoded, encoding: 'UTF-8', needsHex: false }; } } catch (e) { // UTF-8解码失败 } } // 默认显示十六进制 return { displayValue: toHexDisplay(value), encoding: 'HEX', needsHex: true }; }; // 检测是否为二进制数据(包含大量不可打印字符) const isBinaryData = (value: string): boolean => { if (!value || value.length === 0) return false; // 检查前 100 个字符中不可打印字符的比例 const sampleSize = Math.min(value.length, 100); let nonPrintableCount = 0; for (let i = 0; i < sampleSize; i++) { const code = value.charCodeAt(i); // 不可打印字符:控制字符(0-31,除了 9, 10, 13)和 DEL(127) if ((code < 32 && code !== 9 && code !== 10 && code !== 13) || code === 127 || code > 255) { nonPrintableCount++; } } // 如果超过 10% 是不可打印字符,认为是二进制数据 return nonPrintableCount / sampleSize > 0.1; }; // 将字符串转换为十六进制显示 const toHexDisplay = (value: string): string => { const bytes: string[] = []; const ascii: string[] = []; let result = ''; for (let i = 0; i < value.length; i++) { const code = value.charCodeAt(i); bytes.push(code.toString(16).padStart(2, '0').toUpperCase()); // 可打印 ASCII 字符显示原字符,否则显示点 ascii.push(code >= 32 && code < 127 ? value[i] : '.'); if (bytes.length === 16 || i === value.length - 1) { const offset = (Math.floor(i / 16) * 16).toString(16).padStart(8, '0').toUpperCase(); const hexPart = bytes.join(' ').padEnd(47, ' '); const asciiPart = ascii.join(''); result += `${offset} ${hexPart} |${asciiPart}|\n`; bytes.length = 0; ascii.length = 0; } } return result; }; // 尝试解析并格式化 JSON const tryFormatJson = (value: string): { isJson: boolean; formatted: string } => { try { const parsed = JSON.parse(value); return { isJson: true, formatted: JSON.stringify(parsed, null, 2) }; } catch { return { isJson: false, formatted: value }; } }; // 格式化字符串值 - 支持 JSON、二进制数据检测和智能解码 const formatStringValue = (value: string): { displayValue: string; isBinary: boolean; isJson: boolean; encoding?: string } => { // 先检测是否为二进制数据 if (isBinaryData(value)) { const { displayValue, encoding, needsHex } = tryDecodeValue(value); return { displayValue, isBinary: needsHex, isJson: false, encoding }; } // 尝试 JSON 格式化 const { isJson, formatted } = tryFormatJson(value); return { displayValue: formatted, isBinary: false, isJson, encoding: 'UTF-8' }; }; // 可拖拽分隔条组件 - 使用直接 DOM 操作避免卡顿 const ResizableDivider: React.FC<{ onResizeEnd: (newWidth: number) => void; targetRef: React.RefObject; minWidth?: number; }> = ({ onResizeEnd, targetRef, minWidth = 300 }) => { const handleMouseDown = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); const target = targetRef.current; if (!target) return; const startX = e.clientX; const startWidth = target.offsetWidth; const containerWidth = target.parentElement?.offsetWidth || window.innerWidth; const maxWidth = containerWidth - 350; // 右侧至少留 350px // 创建遮罩层防止文本选择和其他交互 const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;cursor:col-resize;z-index:9999;'; document.body.appendChild(overlay); let currentWidth = startWidth; const handleMouseMove = (moveEvent: MouseEvent) => { moveEvent.preventDefault(); const delta = moveEvent.clientX - startX; currentWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + delta)); // 直接操作 DOM,不触发 React 重渲染 target.style.width = `${currentWidth}px`; target.style.flexBasis = `${currentWidth}px`; }; const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); document.body.removeChild(overlay); // 拖拽结束时才更新 React state onResizeEnd(currentWidth); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; return (
(e.currentTarget.style.background = '#d9d9d9')} onMouseLeave={(e) => (e.currentTarget.style.background = '#f0f0f0')} >
); }; // 可拖拽列头组件 - 纯 DOM 操作实现 type RedisKeyTreeLeaf = { keyInfo: RedisKeyInfo; label: string; }; type RedisKeyTreeGroup = { name: string; path: string; children: Map; leaves: RedisKeyTreeLeaf[]; }; type RedisKeyTreeResult = { treeData: DataNode[]; rawKeyByNodeKey: Map; leafNodeKeyByRawKey: Map; groupKeys: string[]; }; const normalizeKeySegment = (segment: string): string => { return segment === '' ? EMPTY_SEGMENT_LABEL : segment; }; const createTreeGroup = (name: string, path: string): RedisKeyTreeGroup => { return { name, path, children: new Map(), leaves: [] }; }; const countGroupLeafNodes = (group: RedisKeyTreeGroup): number => { let count = group.leaves.length; group.children.forEach((child) => { count += countGroupLeafNodes(child); }); return count; }; const buildRedisKeyTree = ( keys: RedisKeyInfo[], formatTTL: (ttl: number) => string, getTypeColor: (type: string) => string ): RedisKeyTreeResult => { const root = createTreeGroup('__root__', '__root__'); keys.forEach((keyInfo) => { const segments = keyInfo.key.split(KEY_GROUP_DELIMITER); if (segments.length <= 1) { root.leaves.push({ keyInfo, label: keyInfo.key }); return; } const groupSegments = segments.slice(0, -1); const leafLabel = normalizeKeySegment(segments[segments.length - 1]); let current = root; const pathParts: string[] = []; groupSegments.forEach((segment) => { const normalized = normalizeKeySegment(segment); pathParts.push(normalized); const groupPath = pathParts.join(KEY_GROUP_DELIMITER); let child = current.children.get(normalized); if (!child) { child = createTreeGroup(normalized, groupPath); current.children.set(normalized, child); } current = child; }); current.leaves.push({ keyInfo, label: leafLabel }); }); const rawKeyByNodeKey = new Map(); const leafNodeKeyByRawKey = new Map(); const groupKeys: string[] = []; const toTreeNodes = (group: RedisKeyTreeGroup): DataNode[] => { const childGroups = Array.from(group.children.values()).sort((a, b) => a.name.localeCompare(b.name)); const childLeaves = [...group.leaves].sort((a, b) => a.keyInfo.key.localeCompare(b.keyInfo.key)); const groupNodes: DataNode[] = childGroups.map((child) => { const groupNodeKey = `group:${child.path}`; groupKeys.push(groupNodeKey); return { key: groupNodeKey, title: ( {child.name} ({countGroupLeafNodes(child)}) ), selectable: false, disableCheckbox: true, children: toTreeNodes(child), }; }); const leafNodes: DataNode[] = childLeaves.map((leaf) => { const nodeKey = `key:${leaf.keyInfo.key}`; rawKeyByNodeKey.set(nodeKey, leaf.keyInfo.key); leafNodeKeyByRawKey.set(leaf.keyInfo.key, nodeKey); return { key: nodeKey, isLeaf: true, title: (
{leaf.label} {leaf.keyInfo.type} {formatTTL(leaf.keyInfo.ttl)}
), }; }); return [...groupNodes, ...leafNodes]; }; return { treeData: toTreeNodes(root), rawKeyByNodeKey, leafNodeKeyByRawKey, groupKeys, }; }; const RedisViewer: React.FC = ({ connectionId, redisDB }) => { const { connections } = useStore(); const connection = connections.find(c => c.id === connectionId); const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(false); const [searchPattern, setSearchPattern] = useState('*'); const [cursor, setCursor] = useState(0); const [hasMore, setHasMore] = useState(false); const [selectedKey, setSelectedKey] = useState(null); const [keyValue, setKeyValue] = useState(null); const [valueLoading, setValueLoading] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); const [newKeyModalOpen, setNewKeyModalOpen] = useState(false); const [newKeyForm] = Form.useForm(); const [ttlModalOpen, setTtlModalOpen] = useState(false); const [ttlForm] = Form.useForm(); const [selectedKeys, setSelectedKeys] = useState([]); const [editValue, setEditValue] = useState(''); // 视图模式状态(用于所有数据类型) const [viewMode, setViewMode] = useState<'auto' | 'text' | 'utf8' | 'hex'>('auto'); // JSON 编辑弹窗状态 const [jsonEditModalOpen, setJsonEditModalOpen] = useState(false); const [jsonEditConfig, setJsonEditConfig] = useState<{ title: string; value: string; isJson: boolean; onSave: (newValue: string) => Promise; } | null>(null); const jsonEditValueRef = useRef(''); // 面板宽度状态和 ref - 默认占据 50% 宽度 const [leftPanelWidth, setLeftPanelWidth] = useState('50%'); const leftPanelRef = useRef(null); const [expandedGroupKeys, setExpandedGroupKeys] = useState([]); const getConfig = useCallback(() => { if (!connection) return null; return { ...connection.config, port: Number(connection.config.port), password: connection.config.password || "", useSSH: connection.config.useSSH || false, ssh: connection.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" }, redisDB: redisDB }; }, [connection, redisDB]); const loadKeys = useCallback(async (pattern: string = '*', fromCursor: number = 0, append: boolean = false) => { const config = getConfig(); if (!config) return; setLoading(true); try { const res = await (window as any).go.app.App.RedisScanKeys(config, pattern, fromCursor, 100); if (res.success) { const result = res.data; if (append) { setKeys(prev => { const keyMap = new Map(); prev.forEach(item => keyMap.set(item.key, item)); result.keys.forEach((item: RedisKeyInfo) => keyMap.set(item.key, item)); return Array.from(keyMap.values()); }); } else { setKeys(result.keys); } setCursor(result.cursor); setHasMore(result.cursor !== 0); } else { message.error('加载 Key 失败: ' + res.message); } } catch (e: any) { message.error('加载 Key 失败: ' + (e?.message || String(e))); } finally { setLoading(false); } }, [getConfig]); useEffect(() => { loadKeys(searchPattern, 0, false); }, [redisDB]); const handleSearch = (value: string) => { const pattern = value.trim() || '*'; setSearchPattern(pattern); setCursor(0); loadKeys(pattern, 0, false); }; const handleLoadMore = () => { loadKeys(searchPattern, cursor, true); }; const handleRefresh = () => { setCursor(0); loadKeys(searchPattern, 0, false); }; const loadKeyValue = async (key: string) => { const config = getConfig(); if (!config) return; setValueLoading(true); try { const res = await (window as any).go.app.App.RedisGetValue(config, key); if (res.success) { setKeyValue(res.data); setSelectedKey(key); } else { message.error('获取值失败: ' + res.message); } } catch (e: any) { message.error('获取值失败: ' + (e?.message || String(e))); } finally { setValueLoading(false); } }; const handleDeleteKeys = async (keysToDelete: string[]) => { const config = getConfig(); if (!config) return; try { const res = await (window as any).go.app.App.RedisDeleteKeys(config, keysToDelete); if (res.success) { message.success(`已删除 ${res.data.deleted} 个 Key`); setKeys(prev => prev.filter(k => !keysToDelete.includes(k.key))); if (selectedKey && keysToDelete.includes(selectedKey)) { setSelectedKey(null); setKeyValue(null); } setSelectedKeys([]); } else { message.error('删除失败: ' + res.message); } } catch (e: any) { message.error('删除失败: ' + (e?.message || String(e))); } }; const handleDeleteCurrentKey = async () => { if (!selectedKey) return; await handleDeleteKeys([selectedKey]); }; const handleSetTTL = async () => { const config = getConfig(); if (!config || !selectedKey) return; try { const values = await ttlForm.validateFields(); const res = await (window as any).go.app.App.RedisSetTTL(config, selectedKey, values.ttl); if (res.success) { message.success('TTL 设置成功'); setTtlModalOpen(false); loadKeyValue(selectedKey); handleRefresh(); } else { message.error('设置失败: ' + res.message); } } catch (e: any) { message.error('设置失败: ' + (e?.message || String(e))); } }; const handleSaveString = async () => { const config = getConfig(); if (!config || !selectedKey) return; try { const res = await (window as any).go.app.App.RedisSetString(config, selectedKey, editValue, keyValue?.ttl || -1); if (res.success) { message.success('保存成功'); setEditModalOpen(false); loadKeyValue(selectedKey); } else { message.error('保存失败: ' + res.message); } } catch (e: any) { message.error('保存失败: ' + (e?.message || String(e))); } }; const handleCreateKey = async () => { const config = getConfig(); if (!config) return; try { const values = await newKeyForm.validateFields(); const res = await (window as any).go.app.App.RedisSetString(config, values.key, values.value, values.ttl || -1); if (res.success) { message.success('创建成功'); setNewKeyModalOpen(false); newKeyForm.resetFields(); handleRefresh(); } else { message.error('创建失败: ' + res.message); } } catch (e: any) { message.error('创建失败: ' + (e?.message || String(e))); } }; const getTypeColor = (type: string) => { switch (type) { case 'string': return 'green'; case 'hash': return 'blue'; case 'list': return 'orange'; case 'set': return 'purple'; case 'zset': return 'magenta'; default: return 'default'; } }; const formatTTL = (ttl: number) => { if (ttl === -1) return '永久'; if (ttl === -2) return '已过期'; if (ttl < 60) return `${ttl}秒`; if (ttl < 3600) return `${Math.floor(ttl / 60)}分${ttl % 60}秒`; if (ttl < 86400) return `${Math.floor(ttl / 3600)}时${Math.floor((ttl % 3600) / 60)}分`; return `${Math.floor(ttl / 86400)}天${Math.floor((ttl % 86400) / 3600)}时`; }; const keyTree = useMemo(() => { return buildRedisKeyTree(keys, formatTTL, getTypeColor); }, [keys]); const selectedTreeNodeKeys = useMemo(() => { if (!selectedKey) { return [] as string[]; } const nodeKey = keyTree.leafNodeKeyByRawKey.get(selectedKey); return nodeKey ? [nodeKey] : []; }, [selectedKey, keyTree]); const checkedTreeNodeKeys = useMemo(() => { return selectedKeys .map(rawKey => keyTree.leafNodeKeyByRawKey.get(rawKey)) .filter((nodeKey): nodeKey is string => Boolean(nodeKey)); }, [selectedKeys, keyTree]); useEffect(() => { const existingKeySet = new Set(keys.map(item => item.key)); setSelectedKeys(prev => prev.filter(rawKey => existingKeySet.has(rawKey))); }, [keys]); useEffect(() => { setExpandedGroupKeys((prev) => { const validKeys = prev.filter(nodeKey => keyTree.groupKeys.includes(nodeKey)); return validKeys; }); }, [keyTree]); const handleTreeSelect = (nodeKeys: React.Key[]) => { if (nodeKeys.length === 0) { return; } const rawKey = keyTree.rawKeyByNodeKey.get(String(nodeKeys[0])); if (!rawKey) { return; } loadKeyValue(rawKey); }; const handleTreeCheck = (checked: React.Key[] | { checked: React.Key[]; halfChecked: React.Key[] }) => { const checkedNodeKeys = Array.isArray(checked) ? checked : checked.checked; const rawKeys = checkedNodeKeys .map(nodeKey => keyTree.rawKeyByNodeKey.get(String(nodeKey))) .filter((rawKey): rawKey is string => Boolean(rawKey)); setSelectedKeys(rawKeys); }; const renderValueEditor = () => { if (!keyValue || !selectedKey) { return
选择一个 Key 查看详情
; } const renderStringValue = () => { const strValue = String(keyValue.value); // 根据查看模式生成显示内容 const getDisplayContent = () => { if (viewMode === 'hex') { return { displayValue: toHexDisplay(strValue), isBinary: true, encoding: 'HEX' }; } else if (viewMode === 'text') { return { displayValue: strValue, isBinary: false, encoding: 'Text' }; } else if (viewMode === 'utf8') { try { const bytes = new Uint8Array(strValue.length); for (let i = 0; i < strValue.length; i++) { bytes[i] = strValue.charCodeAt(i) & 0xFF; } const decoded = new TextDecoder('utf-8', { fatal: false }).decode(bytes); return { displayValue: decoded, isBinary: false, encoding: 'UTF-8' }; } catch (e) { return { displayValue: strValue, isBinary: false, encoding: 'UTF-8 (失败)' }; } } else { // auto mode const { displayValue, isBinary, isJson, encoding } = formatStringValue(strValue); return { displayValue, isBinary, encoding }; } }; const { displayValue, isBinary, encoding } = getDisplayContent(); const isJson = viewMode === 'auto' && formatStringValue(strValue).isJson; return (
{encoding && `编码: ${encoding}`} setViewMode(e.target.value)}> 自动 原始文本 UTF-8 十六进制
{!isBinary && viewMode === 'auto' && ( )} {(isBinary || viewMode !== 'auto') && ( {viewMode !== 'auto' ? '切换到"自动"模式以编辑' : '二进制数据不支持编辑'} )}
); }; const renderHashValue = () => { // 根据查看模式处理值 const processValue = (value: string) => { if (viewMode === 'hex') { return { displayValue: toHexDisplay(value), isBinary: true, isJson: false, encoding: 'HEX' }; } else if (viewMode === 'text') { return { displayValue: value, isBinary: false, isJson: false, encoding: 'Text' }; } else if (viewMode === 'utf8') { try { const bytes = new Uint8Array(value.length); for (let i = 0; i < value.length; i++) { bytes[i] = value.charCodeAt(i) & 0xFF; } const decoded = new TextDecoder('utf-8', { fatal: false }).decode(bytes); return { displayValue: decoded, isBinary: false, isJson: false, encoding: 'UTF-8' }; } catch (e) { return { displayValue: value, isBinary: false, isJson: false, encoding: 'UTF-8 (失败)' }; } } else { // auto mode return formatStringValue(value); } }; const data = Object.entries(keyValue.value as Record).map(([field, value]) => { const { displayValue, isBinary, isJson, encoding } = processValue(value); return { field, value, displayValue, isBinary, isJson, encoding }; }); const handleEditHashField = async (field: string, newValue: string) => { const config = getConfig(); if (!config) return; try { const res = await (window as any).go.app.App.RedisSetHashField(config, selectedKey, field, newValue); if (res.success) { message.success('修改成功'); loadKeyValue(selectedKey); } else { message.error('修改失败: ' + res.message); } } catch (e: any) { message.error('修改失败: ' + (e?.message || String(e))); } }; const handleDeleteHashField = async (field: string) => { const config = getConfig(); if (!config) return; try { const res = await (window as any).go.app.App.RedisDeleteHashField(config, selectedKey, field); if (res.success) { message.success('删除成功'); loadKeyValue(selectedKey); } else { message.error('删除失败: ' + res.message); } } catch (e: any) { message.error('删除失败: ' + (e?.message || String(e))); } }; return (
setViewMode(e.target.value)}> 自动 原始文本 UTF-8 十六进制
{ const tooltipContent = record.encoding && record.encoding !== 'UTF-8' ? `[${record.encoding}]\n${text}` : text; return ( {tooltipContent}} styles={{ root: { maxWidth: 600 } }}> {text} ); } }, { title: '操作', key: 'action', width: 120, render: (_: any, record: any) => ( setViewMode(e.target.value)}> 自动 原始文本 UTF-8 十六进制
{ const tooltipContent = record.encoding && record.encoding !== 'UTF-8' ? `[${record.encoding}]\n${text}` : text; return ( {tooltipContent}} styles={{ root: { maxWidth: 600 } }}> {text} ); } }, { title: '操作', key: 'action', width: 80, render: (_: any, record: any) => ( setViewMode(e.target.value)}> 自动 原始文本 UTF-8 十六进制
{ const tooltipContent = record.encoding && record.encoding !== 'UTF-8' ? `[${record.encoding}]\n${text}` : text; return ( {tooltipContent}} styles={{ root: { maxWidth: 600 } }}> {text} ); } }, { title: '操作', key: 'action', width: 80, render: (_: any, record: any) => ( setViewMode(e.target.value)}> 自动 原始文本 UTF-8 十六进制
{ const tooltipContent = record.encoding && record.encoding !== 'UTF-8' ? `[${record.encoding}]\n${text}` : text; return ( {tooltipContent}} styles={{ root: { maxWidth: 600 } }}> {text} ); } }, { title: '操作', key: 'action', width: 120, render: (_: any, record: any) => (
{keyValue.type === 'string' && renderStringValue()} {keyValue.type === 'hash' && renderHashValue()} {keyValue.type === 'list' && renderListValue()} {keyValue.type === 'set' && renderSetValue()} {keyValue.type === 'zset' && renderZSetValue()}
); }; if (!connection) { return
连接不存在
; } return (
{/* Left: Key List */}
} />
handleDeleteKeys(selectedKeys)} disabled={selectedKeys.length === 0} >
setExpandedGroupKeys(nextExpandedKeys as string[])} onSelect={(nodeKeys) => handleTreeSelect(nodeKeys)} onCheck={(checked) => handleTreeCheck(checked)} style={{ padding: '8px 6px' }} /> {hasMore && (
)}
{/* Resizable Divider */} {/* Right: Value Viewer */}
{valueLoading ? (
加载中...
) : ( renderValueEditor() )}
{/* Edit String Modal */} setEditModalOpen(false)} width={800} styles={{ body: { height: 500 } }} > setEditValue(value || '')} options={{ minimap: { enabled: false }, lineNumbers: 'on', wordWrap: 'on', scrollBeyondLastLine: false, automaticLayout: true, folding: true }} /> {/* New Key Modal */} setNewKeyModalOpen(false)} >
{/* TTL Modal */} setTtlModalOpen(false)} >
{/* JSON Edit Modal with Monaco Editor */} { if (jsonEditConfig?.onSave) { await jsonEditConfig.onSave(jsonEditValueRef.current); } setJsonEditModalOpen(false); }} onCancel={() => setJsonEditModalOpen(false)} width={800} styles={{ body: { height: 500 } }} > { jsonEditValueRef.current = value || ''; }} onMount={(editor) => { jsonEditValueRef.current = jsonEditConfig?.value || ''; }} options={{ minimap: { enabled: false }, lineNumbers: 'on', wordWrap: 'on', scrollBeyondLastLine: false, automaticLayout: true, folding: true, formatOnPaste: true }} />
); }; export default RedisViewer;