Files
opsxsql/frontend/src/pages/RedisDetailPage.tsx
T
logan 6a8c4d95d5 feat: Redis/MySQL 加载骨架屏 loading 效果
- SchemaSidebar: MySQL 表列表加载改为 6 行骨架屏 animate-pulse
- SchemaSidebar: Redis key 列表加载改为 6 行骨架屏 animate-pulse
- RedisDetailPage: key 表格加载时显示 5 行骨架屏 + 加载中提示
- 替换原来的纯文字/spinner,视觉上更流畅
2026-05-24 15:46:58 +08:00

310 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, Key, Activity, Terminal, Gauge, Plus, RefreshCw, Search } from 'lucide-react';
import { redisApi } from '@/api/redis';
type TabKey = 'keys' | 'monitor' | 'cli' | 'perf';
export default function RedisDetailPage() {
var params = useParams<{ id: string }>();
var navigate = useNavigate();
var instanceId = Number(params.id);
var [info, setInfo] = useState<Record<string, any>>({});
var [activeTab, setActiveTab] = useState<TabKey>('keys');
var [selectedDb, setSelectedDb] = useState(0);
var [keyPattern, setKeyPattern] = useState('*');
var [keyType, setKeyType] = useState('');
var [keys, setKeys] = useState<any[]>([]);
var [selectedKey, setSelectedKey] = useState<any>(null);
var [showAddKey, setShowAddKey] = useState(false);
var [newKey, setNewKey] = useState({ key: '', value: '', type: 'string', ttl: -1 });
var [cmdInput, setCmdInput] = useState('');
var [cmdOutput, setCmdOutput] = useState<string[]>([]);
var [slowlog, setSlowlog] = useState<any[]>([]);
var [clients, setClients] = useState<any[]>([]);
var [loading, setLoading] = useState(false);
var tabs: { key: TabKey; label: string; icon: React.ElementType }[] = [
{ key: 'keys', label: '键管理', icon: Key },
{ key: 'monitor', label: '监控信息', icon: Activity },
{ key: 'cli', label: '命令行', icon: Terminal },
{ key: 'perf', label: '性能分析', icon: Gauge },
];
useEffect(function () {
redisApi.getInfo(instanceId).then(function (res: any) {
setInfo(res.info || res || {});
}).catch(function () { setInfo({}); });
}, [instanceId]);
var fetchKeys = function () {
setLoading(true);
redisApi.getKeys(instanceId, { db: selectedDb, pattern: keyPattern, type: keyType || undefined, count: 50 }).then(function (res: any) {
setKeys(res.keys || res || []);
}).catch(function () { setKeys([]); }).finally(function () { setLoading(false); });
};
var handleAddKey = function () {
redisApi.createKey(instanceId, { db: selectedDb, key: newKey.key, value: newKey.value, type: newKey.type, ttl: newKey.ttl > 0 ? newKey.ttl : undefined }).then(function () {
setShowAddKey(false);
setNewKey({ key: '', value: '', type: 'string', ttl: -1 });
fetchKeys();
}).catch(function () {});
};
var handleDeleteKey = function (keyName: string) {
if (!confirm('确定删除键 ' + keyName + '?')) return;
redisApi.deleteKey(instanceId, keyName, selectedDb).then(function () { fetchKeys(); }).catch(function () {});
};
var handleCommand = function () {
if (!cmdInput.trim()) return;
var cmd = cmdInput;
setCmdInput('');
setCmdOutput(function (prev) { return prev.concat(['> ' + cmd]); });
redisApi.executeCommand(instanceId, { db: selectedDb, command: cmd }).then(function (res: any) {
setCmdOutput(function (prev) { return prev.concat([String(res.result || res.data || JSON.stringify(res))]); });
}).catch(function (e: any) {
setCmdOutput(function (prev) { return prev.concat(['ERROR: ' + (e?.response?.data?.detail || '执行失败')]); });
});
};
useEffect(function () {
if (activeTab === 'perf') {
redisApi.getSlowlog(instanceId, 20).then(function (res: any) { setSlowlog(res.slowlog || res || []); }).catch(function () { setSlowlog([]); });
redisApi.getClients(instanceId).then(function (res: any) { setClients(res.clients || res || []); }).catch(function () { setClients([]); });
}
}, [activeTab, instanceId]);
var infoSections = Object.entries(info).filter(function (e) { return typeof e[1] === 'object' && e[1] !== null; });
var infoScalars = Object.entries(info).filter(function (e) { return typeof e[1] !== 'object'; });
return (
<div className="space-y-5">
{/* 头部 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button onClick={function () { navigate('/instances'); }} className="w-9 h-9 rounded-lg bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] flex items-center justify-center text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]">
<ArrowLeft className="w-4 h-4" />
</button>
<div>
<h1 className="text-xl font-semibold">Redis 实例</h1>
<p className="text-sm text-[var(--dg-text-muted)]">#{instanceId}</p>
</div>
</div>
</div>
{/* 信息卡片 */}
{infoScalars.length > 0 && (
<div className="grid grid-cols-4 gap-3">
{infoScalars.slice(0, 4).map(function (e) {
return <div key={e[0]} className="dg-card p-3"><div className="text-lg font-bold">{String(e[1])}</div><div className="text-xs text-[var(--dg-text-muted)]">{e[0]}</div></div>;
})}
</div>
)}
{/* Tab */}
<div className="flex gap-0 border-b border-[var(--dg-border)]">
{tabs.map(function (tab) {
var Icon = tab.icon;
return <button key={tab.key} onClick={function () { setActiveTab(tab.key); }}
className={'flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors border-b-2 -mb-px '
+ (activeTab === tab.key ? 'border-blue-500 text-blue-400' : 'border-transparent text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]')}>
<Icon className="w-4 h-4" />{tab.label}
</button>;
})}
</div>
{/* 键管理 */}
{activeTab === 'keys' && (
<div className="space-y-4">
<div className="flex items-center gap-3 flex-wrap">
<select value={selectedDb} onChange={function (e) { setSelectedDb(Number(e.target.value)); }} className="px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm w-24">
{Array.from({ length: 16 }, function (_, i) { return i; }).map(function (i) { return <option key={i} value={i}>DB {i}</option>; })}
</select>
<div className="relative flex-1 max-w-xs">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--dg-text-muted)]" />
<input value={keyPattern} onChange={function (e) { setKeyPattern(e.target.value); }} className="w-full pl-10 pr-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm" placeholder="pattern, 如 user:*" />
</div>
<select value={keyType} onChange={function (e) { setKeyType(e.target.value); }} className="px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm">
<option value="">全部类型</option>
<option value="string">String</option><option value="hash">Hash</option><option value="list">List</option><option value="set">Set</option><option value="zset">ZSet</option>
</select>
<button onClick={fetchKeys} disabled={loading} className="px-3 py-2 bg-[var(--dg-bg-tertiary)] rounded-lg text-sm hover:bg-[var(--dg-bg-hover)] disabled:opacity-50"><RefreshCw className="w-4 h-4" /></button>
<button onClick={function () { setShowAddKey(true); }} className="px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm flex items-center gap-1"><Plus className="w-4 h-4" />新增</button>
</div>
{/* 键列表 */}
<div className="overflow-x-auto">
{loading ? (
<div className="space-y-2 p-4">
{Array.from({ length: 5 }, function (_, i) {
return <div key={i} className="flex items-center gap-4">
<div className="h-4 bg-[var(--dg-bg-tertiary)] rounded animate-pulse flex-1" />
<div className="h-4 bg-[var(--dg-bg-tertiary)] rounded animate-pulse w-16" />
<div className="h-4 bg-[var(--dg-bg-tertiary)] rounded animate-pulse w-12" />
<div className="h-4 bg-[var(--dg-bg-tertiary)] rounded animate-pulse w-20" />
</div>;
})}
<div className="text-center text-xs text-[var(--dg-text-muted)] pt-2">加载中...</div>
</div>
) : keys.length === 0 ? (
<div className="text-center py-8 text-[var(--dg-text-muted)]">点击刷新加载键列表</div>
) : (
<table className="w-full text-sm">
<thead><tr className="text-left text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
<th className="px-4 py-2">键名</th><th className="px-4 py-2">类型</th><th className="px-4 py-2">TTL</th><th className="px-4 py-2">操作</th>
</tr></thead>
<tbody>
{keys.map(function (k: any, i: number) {
return <tr key={i} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
<td className="px-4 py-2 font-mono text-xs">{k.key || k.name || k}</td>
<td className="px-4 py-2">{k.type || '-'}</td>
<td className="px-4 py-2">{k.ttl != null ? k.ttl : '-'}</td>
<td className="px-4 py-2 flex gap-2">
<button onClick={function () {
redisApi.getKeyDetail(instanceId, k.key || k.name || k, selectedDb).then(function (res: any) { setSelectedKey(res); }).catch(function () {});
}} className="text-xs text-blue-400 hover:text-blue-300">查看</button>
<button onClick={function () { handleDeleteKey(k.key || k.name || k); }} className="text-xs text-red-400 hover:text-red-300">删除</button>
</td>
</tr>;
})}
</tbody>
</table>
)}
</div>
</div>
)}
{/* 监控信息 */}
{activeTab === 'monitor' && (
<div className="space-y-4">
{infoSections.map(function (section) {
var entries = Object.entries(section[1] as Record<string, any>);
return (
<div key={section[0]} className="dg-card p-4">
<h3 className="text-sm font-semibold mb-3">{section[0]}</h3>
<div className="grid grid-cols-3 gap-2">
{entries.map(function (e) {
return <div key={e[0]} className="flex justify-between px-3 py-1.5 bg-[var(--dg-bg-tertiary)] rounded text-xs"><span className="text-[var(--dg-text-muted)]">{e[0]}</span><span className="font-mono">{String(e[1])}</span></div>;
})}
</div>
</div>
);
})}
</div>
)}
{/* 命令行 */}
{activeTab === 'cli' && (
<div className="space-y-4">
<div className="flex items-center gap-3">
<select value={selectedDb} onChange={function (e) { setSelectedDb(Number(e.target.value)); }} className="px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm w-24">
{Array.from({ length: 16 }, function (_, i) { return i; }).map(function (i) { return <option key={i} value={i}>DB {i}</option>; })}
</select>
<input value={cmdInput} onChange={function (e) { setCmdInput(e.target.value); }}
onKeyDown={function (e) { if (e.key === 'Enter') handleCommand(); }}
className="flex-1 px-3 py-2 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm font-mono" placeholder="输入 Redis 命令..." />
<button onClick={handleCommand} className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm">执行</button>
</div>
<div className="dg-card p-4 min-h-[300px] max-h-[400px] overflow-y-auto font-mono text-xs space-y-1">
{cmdOutput.length === 0 && <div className="text-[var(--dg-text-muted)]">等待输入命令...</div>}
{cmdOutput.map(function (line, i) {
return <div key={i} className={line.startsWith('>') ? 'text-green-400' : line.startsWith('ERROR') ? 'text-red-400' : 'text-[var(--dg-text-secondary)]'}>{line}</div>;
})}
</div>
</div>
)}
{/* 性能分析 */}
{activeTab === 'perf' && (
<div className="space-y-4">
<div className="dg-card p-4">
<h3 className="text-sm font-semibold mb-3">慢日志</h3>
<table className="w-full text-sm">
<thead><tr className="text-left text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
<th className="px-4 py-2">命令</th><th className="px-4 py-2">耗时(μs)</th><th className="px-4 py-2">时间</th>
</tr></thead>
<tbody>
{slowlog.map(function (s: any, i: number) {
return <tr key={i} className="border-b border-[var(--dg-border)]">
<td className="px-4 py-2 font-mono text-xs">{Array.isArray(s.command) ? s.command.join(' ') : s.command || '-'}</td>
<td className="px-4 py-2">{s.duration || s.execution_time || '-'}</td>
<td className="px-4 py-2">{s.timestamp || '-'}</td>
</tr>;
})}
</tbody>
</table>
{slowlog.length === 0 && <div className="text-center py-4 text-[var(--dg-text-muted)] text-xs">暂无慢日志</div>}
</div>
<div className="dg-card p-4">
<h3 className="text-sm font-semibold mb-3">客户端列表</h3>
<table className="w-full text-sm">
<thead><tr className="text-left text-[var(--dg-text-muted)] border-b border-[var(--dg-border)]">
<th className="px-4 py-2">ID</th><th className="px-4 py-2">地址</th><th className="px-4 py-2">DB</th><th className="px-4 py-2">空闲(s)</th>
</tr></thead>
<tbody>
{clients.map(function (c: any, i: number) {
return <tr key={i} className="border-b border-[var(--dg-border)]">
<td className="px-4 py-2">{c.id || c.fd || '-'}</td>
<td className="px-4 py-2">{c.addr || c.host || '-'}</td>
<td className="px-4 py-2">{c.db || '-'}</td>
<td className="px-4 py-2">{c.idle || '-'}</td>
</tr>;
})}
</tbody>
</table>
</div>
</div>
)}
{/* 键详情弹窗 */}
{selectedKey && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-[var(--dg-bg-secondary)] rounded-xl shadow-2xl w-full max-w-lg max-h-[70vh] overflow-y-auto p-6" onClick={function (e) { e.stopPropagation(); }}>
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold">键详情</h3>
<button onClick={function () { setSelectedKey(null); }} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">✕</button>
</div>
<div className="space-y-3">
<div className="flex gap-2 text-sm"><span className="text-[var(--dg-text-muted)]">键名:</span><span className="font-mono">{selectedKey.key || selectedKey.name}</span></div>
<div className="flex gap-2 text-sm"><span className="text-[var(--dg-text-muted)]">类型:</span><span>{selectedKey.type}</span></div>
<div className="flex gap-2 text-sm"><span className="text-[var(--dg-text-muted)]">TTL:</span><span>{selectedKey.ttl != null ? selectedKey.ttl + 's' : '-'}</span></div>
<div className="text-sm"><span className="text-[var(--dg-text-muted)]">值:</span>
<pre className="mt-1 p-3 bg-[var(--dg-bg-tertiary)] rounded-lg text-xs font-mono max-h-60 overflow-y-auto">
{typeof selectedKey.value === 'object' ? JSON.stringify(selectedKey.value, null, 2) : String(selectedKey.value || '-')}
</pre>
</div>
</div>
</div>
</div>
)}
{/* 新增键弹窗 */}
{showAddKey && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-[var(--dg-bg-secondary)] rounded-xl shadow-2xl w-full max-w-md p-6" onClick={function (e) { e.stopPropagation(); }}>
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold">新增键</h3>
<button onClick={function () { setShowAddKey(false); }} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">✕</button>
</div>
<div className="space-y-3">
<div><label className="text-sm text-[var(--dg-text-muted)]">类型</label><select value={newKey.type} onChange={function (e) { setNewKey(Object.assign({}, newKey, { type: e.target.value })); }} className="w-full px-3 py-2 mt-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm">
<option value="string">String</option><option value="hash">Hash</option><option value="list">List</option><option value="set">Set</option><option value="zset">ZSet</option>
</select></div>
<div><label className="text-sm text-[var(--dg-text-muted)]">键名</label><input value={newKey.key} onChange={function (e) { setNewKey(Object.assign({}, newKey, { key: e.target.value })); }} className="w-full px-3 py-2 mt-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm" placeholder="key" /></div>
<div><label className="text-sm text-[var(--dg-text-muted)]">值</label><textarea value={newKey.value} onChange={function (e) { setNewKey(Object.assign({}, newKey, { value: e.target.value })); }} className="w-full px-3 py-2 mt-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm font-mono min-h-[80px]" placeholder="value" /></div>
<div><label className="text-sm text-[var(--dg-text-muted)]">TTL (秒, -1 永不过期)</label><input type="number" value={newKey.ttl} onChange={function (e) { setNewKey(Object.assign({}, newKey, { ttl: Number(e.target.value) })); }} className="w-full px-3 py-2 mt-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg text-sm" /></div>
</div>
<div className="flex justify-end gap-3 mt-4">
<button onClick={function () { setShowAddKey(false); }} className="px-4 py-2 bg-[var(--dg-bg-tertiary)] rounded-lg text-sm">取消</button>
<button onClick={handleAddKey} className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm">添加</button>
</div>
</div>
</div>
)}
</div>
);
}