1292 lines
43 KiB
JavaScript
1292 lines
43 KiB
JavaScript
/**
|
||
* Redis比较工具的JavaScript功能
|
||
* 支持Redis集群配置、连接测试、数据比较等功能
|
||
*/
|
||
|
||
// 全局变量
|
||
let currentResults = null;
|
||
let isQuerying = false;
|
||
let currentImportTarget = null; // 记录当前导入目标集群
|
||
|
||
// 页面加载完成后初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
initializePage();
|
||
bindEvents();
|
||
loadRedisConfigGroups();
|
||
loadRedisQueryHistory();
|
||
});
|
||
|
||
/**
|
||
* 初始化页面
|
||
*/
|
||
function initializePage() {
|
||
// 加载默认配置
|
||
loadDefaultConfig();
|
||
|
||
// 绑定节点删除事件
|
||
bindNodeEvents();
|
||
|
||
// 绑定查询模式切换事件
|
||
bindQueryModeEvents();
|
||
}
|
||
|
||
/**
|
||
* 绑定事件
|
||
*/
|
||
function bindEvents() {
|
||
// 查询模式切换
|
||
document.querySelectorAll('input[name="queryMode"]').forEach(radio => {
|
||
radio.addEventListener('change', toggleQueryMode);
|
||
});
|
||
|
||
// 节点删除按钮事件委托
|
||
document.addEventListener('click', function(e) {
|
||
if (e.target.closest('.remove-node')) {
|
||
e.target.closest('.node-input').remove();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 绑定节点相关事件
|
||
*/
|
||
function bindNodeEvents() {
|
||
// 初始绑定现有的删除按钮(如果只有一个节点则禁用删除)
|
||
updateNodeDeleteButtons();
|
||
}
|
||
|
||
/**
|
||
* 绑定查询模式切换事件
|
||
*/
|
||
function bindQueryModeEvents() {
|
||
const randomMode = document.getElementById('randomMode');
|
||
const specifiedMode = document.getElementById('specifiedMode');
|
||
|
||
randomMode.addEventListener('change', toggleQueryMode);
|
||
specifiedMode.addEventListener('change', toggleQueryMode);
|
||
}
|
||
|
||
/**
|
||
* 切换查询模式
|
||
*/
|
||
function toggleQueryMode() {
|
||
const randomMode = document.getElementById('randomMode').checked;
|
||
const randomOptions = document.getElementById('randomOptions');
|
||
const specifiedOptions = document.getElementById('specifiedOptions');
|
||
|
||
if (randomMode) {
|
||
randomOptions.style.display = 'block';
|
||
specifiedOptions.style.display = 'none';
|
||
} else {
|
||
randomOptions.style.display = 'none';
|
||
specifiedOptions.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 添加节点
|
||
*/
|
||
function addNode(clusterId) {
|
||
const container = document.getElementById(`${clusterId}-nodes`);
|
||
const nodeInput = document.createElement('div');
|
||
nodeInput.className = 'node-input';
|
||
nodeInput.innerHTML = `
|
||
<input type="text" class="form-control node-host" placeholder="主机地址" value="127.0.0.1">
|
||
<input type="number" class="form-control node-port" placeholder="端口" value="7000" style="width: 100px;">
|
||
<button type="button" class="btn btn-outline-danger btn-sm remove-node">
|
||
<i class="fas fa-minus"></i>
|
||
</button>
|
||
`;
|
||
container.appendChild(nodeInput);
|
||
updateNodeDeleteButtons();
|
||
}
|
||
|
||
/**
|
||
* 更新节点删除按钮状态
|
||
*/
|
||
function updateNodeDeleteButtons() {
|
||
// 每个集群至少需要一个节点
|
||
['cluster1', 'cluster2'].forEach(clusterId => {
|
||
const container = document.getElementById(`${clusterId}-nodes`);
|
||
const nodeInputs = container.querySelectorAll('.node-input');
|
||
const deleteButtons = container.querySelectorAll('.remove-node');
|
||
|
||
deleteButtons.forEach(btn => {
|
||
btn.disabled = nodeInputs.length <= 1;
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取集群配置
|
||
*/
|
||
function getClusterConfig(clusterId) {
|
||
const name = document.getElementById(`${clusterId}-name`).value;
|
||
const password = document.getElementById(`${clusterId}-password`).value;
|
||
const timeout = parseInt(document.getElementById(`${clusterId}-timeout`).value);
|
||
const maxConn = parseInt(document.getElementById(`${clusterId}-max-conn`).value);
|
||
|
||
// 获取节点列表
|
||
const nodes = [];
|
||
const nodeInputs = document.querySelectorAll(`#${clusterId}-nodes .node-input`);
|
||
|
||
nodeInputs.forEach(nodeInput => {
|
||
const host = nodeInput.querySelector('.node-host').value.trim();
|
||
const port = parseInt(nodeInput.querySelector('.node-port').value);
|
||
|
||
if (host && port) {
|
||
nodes.push({ host, port });
|
||
}
|
||
});
|
||
|
||
return {
|
||
name,
|
||
nodes,
|
||
password: password || null,
|
||
socket_timeout: timeout,
|
||
socket_connect_timeout: timeout,
|
||
max_connections_per_node: maxConn
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 获取查询选项
|
||
*/
|
||
function getQueryOptions() {
|
||
const randomMode = document.getElementById('randomMode').checked;
|
||
|
||
if (randomMode) {
|
||
return {
|
||
mode: 'random',
|
||
count: parseInt(document.getElementById('sampleCount').value),
|
||
pattern: document.getElementById('keyPattern').value,
|
||
source_cluster: document.getElementById('sourceCluster').value
|
||
};
|
||
} else {
|
||
const keysText = document.getElementById('specifiedKeys').value.trim();
|
||
const keys = keysText ? keysText.split('\n').map(k => k.trim()).filter(k => k) : [];
|
||
|
||
return {
|
||
mode: 'specified',
|
||
keys: keys
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 测试连接
|
||
*/
|
||
async function testConnection(clusterId) {
|
||
const config = getClusterConfig(clusterId);
|
||
const clusterName = config.name;
|
||
|
||
if (!config.nodes || config.nodes.length === 0) {
|
||
showAlert('请至少配置一个Redis节点', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showLoading(`正在测试${clusterName}连接...`);
|
||
|
||
const response = await fetch('/api/redis/test-connection', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
cluster_config: config,
|
||
cluster_name: clusterName
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
hideLoading();
|
||
|
||
if (result.success) {
|
||
showAlert(`${clusterName}连接成功!连接耗时: ${result.data.connection_time.toFixed(3)}秒`, 'success');
|
||
|
||
// 高亮成功的集群配置
|
||
document.getElementById(`${clusterId}-config`).classList.add('active');
|
||
setTimeout(() => {
|
||
document.getElementById(`${clusterId}-config`).classList.remove('active');
|
||
}, 2000);
|
||
} else {
|
||
showAlert(`${clusterName}连接失败: ${result.error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
hideLoading();
|
||
showAlert(`连接测试失败: ${error.message}`, 'danger');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行Redis比较
|
||
*/
|
||
async function executeRedisComparison() {
|
||
if (isQuerying) {
|
||
showAlert('查询正在进行中,请稍候...', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 获取配置
|
||
const cluster1Config = getClusterConfig('cluster1');
|
||
const cluster2Config = getClusterConfig('cluster2');
|
||
const queryOptions = getQueryOptions();
|
||
|
||
// 验证配置
|
||
if (!cluster1Config.nodes || cluster1Config.nodes.length === 0) {
|
||
showAlert('请配置集群1的Redis节点', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!cluster2Config.nodes || cluster2Config.nodes.length === 0) {
|
||
showAlert('请配置集群2的Redis节点', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (queryOptions.mode === 'specified' && (!queryOptions.keys || queryOptions.keys.length === 0)) {
|
||
showAlert('请输入要查询的Key列表', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
isQuerying = true;
|
||
showLoading('正在执行Redis数据比较,请稍候...');
|
||
clearResults();
|
||
|
||
const response = await fetch('/api/redis/compare', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
cluster1_config: cluster1Config,
|
||
cluster2_config: cluster2Config,
|
||
query_options: queryOptions
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok && result.success !== false) {
|
||
currentResults = result;
|
||
displayResults(result);
|
||
showAlert('Redis数据比较完成!', 'success');
|
||
} else {
|
||
showAlert(`比较失败: ${result.error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
showAlert(`请求失败: ${error.message}`, 'danger');
|
||
} finally {
|
||
isQuerying = false;
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示结果
|
||
*/
|
||
function displayResults(results) {
|
||
// 显示统计卡片
|
||
displayStatsCards(results.stats);
|
||
|
||
// 显示详细结果
|
||
displayDifferenceResults(results.different_results || []);
|
||
displayIdenticalResults(results.identical_results || []);
|
||
displayMissingResults(results.missing_results || []);
|
||
displayPerformanceReport(results.performance_report);
|
||
|
||
// 更新标签页计数
|
||
updateTabCounts(results);
|
||
|
||
// 显示结果区域
|
||
document.getElementById('results').style.display = 'block';
|
||
|
||
// 滚动到结果区域
|
||
document.getElementById('results').scrollIntoView({ behavior: 'smooth' });
|
||
}
|
||
|
||
/**
|
||
* 显示统计卡片
|
||
*/
|
||
function displayStatsCards(stats) {
|
||
const container = document.getElementById('statsCards');
|
||
container.innerHTML = `
|
||
<div class="col-md-3">
|
||
<div class="stat-card bg-primary text-white">
|
||
<h3>${stats.total_keys}</h3>
|
||
<p>总Key数量</p>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="stat-card bg-success text-white">
|
||
<h3>${stats.identical_count}</h3>
|
||
<p>相同数据</p>
|
||
<small>${stats.identical_percentage}%</small>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="stat-card bg-danger text-white">
|
||
<h3>${stats.different_count}</h3>
|
||
<p>差异数据</p>
|
||
<small>${stats.different_percentage}%</small>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="stat-card bg-warning text-white">
|
||
<h3>${stats.missing_in_cluster1 + stats.missing_in_cluster2 + stats.both_missing}</h3>
|
||
<p>缺失数据</p>
|
||
<small>${stats.missing_percentage}%</small>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 显示差异结果
|
||
*/
|
||
function displayDifferenceResults(differences) {
|
||
const container = document.getElementById('differenceResults');
|
||
|
||
if (differences.length === 0) {
|
||
container.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>未发现数据差异</div>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
differences.forEach((diff, index) => {
|
||
html += `
|
||
<div class="difference-item">
|
||
<h6><i class="fas fa-key me-2"></i>Key: <code>${diff.key}</code></h6>
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<strong>${currentResults.clusters.cluster1_name}:</strong>
|
||
<pre class="redis-value mt-2">${formatRedisValue(diff.cluster1_value)}</pre>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<strong>${currentResults.clusters.cluster2_name}:</strong>
|
||
<pre class="redis-value mt-2">${formatRedisValue(diff.cluster2_value)}</pre>
|
||
</div>
|
||
</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-danger">${diff.message}</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
/**
|
||
* 显示相同结果
|
||
*/
|
||
function displayIdenticalResults(identical) {
|
||
const container = document.getElementById('identicalResults');
|
||
|
||
if (identical.length === 0) {
|
||
container.innerHTML = '<div class="alert alert-info"><i class="fas fa-info-circle me-2"></i>没有相同的数据</div>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
identical.forEach((item, index) => {
|
||
html += `
|
||
<div class="identical-item">
|
||
<h6><i class="fas fa-key me-2"></i>Key: <code>${item.key}</code></h6>
|
||
<div class="mt-2">
|
||
<strong>值:</strong>
|
||
<pre class="redis-value mt-2">${formatRedisValue(item.value)}</pre>
|
||
</div>
|
||
<div class="mt-2">
|
||
<span class="badge bg-success">数据一致</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
/**
|
||
* 显示缺失结果
|
||
*/
|
||
function displayMissingResults(missing) {
|
||
const container = document.getElementById('missingResults');
|
||
|
||
if (missing.length === 0) {
|
||
container.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>没有缺失的数据</div>';
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
missing.forEach((item, index) => {
|
||
html += `
|
||
<div class="missing-item">
|
||
<h6><i class="fas fa-key me-2"></i>Key: <code>${item.key}</code></h6>
|
||
<div class="mt-2">
|
||
<span class="badge bg-warning">${item.message}</span>
|
||
</div>
|
||
${item.cluster1_value !== undefined ? `
|
||
<div class="mt-2">
|
||
<strong>${currentResults.clusters.cluster1_name}:</strong>
|
||
<pre class="redis-value mt-1">${formatRedisValue(item.cluster1_value)}</pre>
|
||
</div>
|
||
` : ''}
|
||
${item.cluster2_value !== undefined ? `
|
||
<div class="mt-2">
|
||
<strong>${currentResults.clusters.cluster2_name}:</strong>
|
||
<pre class="redis-value mt-1">${formatRedisValue(item.cluster2_value)}</pre>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
/**
|
||
* 显示性能报告
|
||
*/
|
||
function displayPerformanceReport(performanceReport) {
|
||
const container = document.getElementById('performanceReport');
|
||
|
||
const connections = performanceReport.connections || {};
|
||
const operations = performanceReport.operations || {};
|
||
|
||
let html = `
|
||
<h5><i class="fas fa-chart-line me-2"></i>性能统计报告</h5>
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<h6>连接统计</h6>
|
||
<table class="table table-sm">
|
||
<thead>
|
||
<tr>
|
||
<th>集群</th>
|
||
<th>状态</th>
|
||
<th>耗时</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
Object.entries(connections).forEach(([clusterName, status]) => {
|
||
const statusClass = status.success ? 'success' : 'danger';
|
||
const statusText = status.success ? '成功' : '失败';
|
||
|
||
html += `
|
||
<tr>
|
||
<td>${clusterName}</td>
|
||
<td><span class="badge bg-${statusClass}">${statusText}</span></td>
|
||
<td>${status.connect_time.toFixed(3)}s</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += `
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<h6>操作统计</h6>
|
||
<table class="table table-sm">
|
||
<thead>
|
||
<tr>
|
||
<th>操作</th>
|
||
<th>耗时</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
`;
|
||
|
||
if (operations.scan_time > 0) {
|
||
html += `<tr><td>扫描Keys</td><td>${operations.scan_time.toFixed(3)}s</td></tr>`;
|
||
}
|
||
|
||
Object.entries(operations.queries || {}).forEach(([operation, duration]) => {
|
||
html += `<tr><td>${operation}</td><td>${duration.toFixed(3)}s</td></tr>`;
|
||
});
|
||
|
||
if (operations.comparison_time > 0) {
|
||
html += `<tr><td>数据比对</td><td>${operations.comparison_time.toFixed(3)}s</td></tr>`;
|
||
}
|
||
|
||
html += `
|
||
<tr class="table-primary">
|
||
<td><strong>总耗时</strong></td>
|
||
<td><strong>${performanceReport.total_time.toFixed(3)}s</strong></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
/**
|
||
* 更新标签页计数
|
||
*/
|
||
function updateTabCounts(results) {
|
||
document.getElementById('diff-count').textContent = (results.different_results || []).length;
|
||
document.getElementById('identical-count').textContent = (results.identical_results || []).length;
|
||
document.getElementById('missing-count').textContent = (results.missing_results || []).length;
|
||
}
|
||
|
||
/**
|
||
* 格式化Redis值显示
|
||
*/
|
||
function formatRedisValue(value) {
|
||
if (value === null) {
|
||
return '(null)';
|
||
}
|
||
|
||
if (value === undefined) {
|
||
return '(undefined)';
|
||
}
|
||
|
||
// 如果是字符串且看起来像JSON,尝试格式化
|
||
if (typeof value === 'string') {
|
||
try {
|
||
const parsed = JSON.parse(value);
|
||
return JSON.stringify(parsed, null, 2);
|
||
} catch (e) {
|
||
// 不是JSON,直接返回
|
||
return value;
|
||
}
|
||
}
|
||
|
||
return String(value);
|
||
}
|
||
|
||
/**
|
||
* 加载默认配置
|
||
*/
|
||
async function loadDefaultConfig() {
|
||
try {
|
||
const response = await fetch('/api/redis/default-config');
|
||
const config = await response.json();
|
||
|
||
// 这里可以根据需要设置默认值,当前HTML已经包含了合理的默认值
|
||
} catch (error) {
|
||
console.warn('加载默认配置失败:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清空结果
|
||
*/
|
||
function clearResults() {
|
||
document.getElementById('results').style.display = 'none';
|
||
currentResults = null;
|
||
}
|
||
|
||
/**
|
||
* 显示加载状态
|
||
*/
|
||
function showLoading(message = '正在处理...') {
|
||
const loadingElement = document.querySelector('.loading');
|
||
const messageElement = loadingElement.querySelector('span');
|
||
messageElement.textContent = message;
|
||
loadingElement.style.display = 'block';
|
||
}
|
||
|
||
/**
|
||
* 隐藏加载状态
|
||
*/
|
||
function hideLoading() {
|
||
document.querySelector('.loading').style.display = 'none';
|
||
}
|
||
|
||
/**
|
||
* 显示提示消息
|
||
*/
|
||
function showAlert(message, type = 'info') {
|
||
// 移除现有的alert
|
||
const existingAlert = document.querySelector('.alert-custom');
|
||
if (existingAlert) {
|
||
existingAlert.remove();
|
||
}
|
||
|
||
// 创建新的alert
|
||
const alertDiv = document.createElement('div');
|
||
alertDiv.className = `alert alert-${type} alert-dismissible fade show alert-custom`;
|
||
alertDiv.innerHTML = `
|
||
${message}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||
`;
|
||
|
||
// 插入到页面顶部
|
||
const container = document.querySelector('.container');
|
||
container.insertBefore(alertDiv, container.firstChild);
|
||
|
||
// 5秒后自动消失
|
||
setTimeout(() => {
|
||
if (alertDiv.parentNode) {
|
||
alertDiv.remove();
|
||
}
|
||
}, 5000);
|
||
}
|
||
|
||
/**
|
||
* Redis配置管理功能
|
||
*/
|
||
|
||
// 加载Redis配置组列表
|
||
async function loadRedisConfigGroups() {
|
||
try {
|
||
const response = await fetch('/api/redis/config-groups');
|
||
const result = await response.json();
|
||
|
||
const select = document.getElementById('redisConfigGroupSelect');
|
||
select.innerHTML = '<option value="">选择Redis配置组...</option>';
|
||
|
||
if (result.success && result.data) {
|
||
result.data.forEach(group => {
|
||
const option = document.createElement('option');
|
||
option.value = group.id;
|
||
option.textContent = `${group.name} - ${group.description || '无描述'}`;
|
||
select.appendChild(option);
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('加载Redis配置组失败:', error);
|
||
}
|
||
}
|
||
|
||
// 显示保存Redis配置对话框
|
||
function showSaveRedisConfigDialog() {
|
||
// 生成默认名称
|
||
const timestamp = new Date().toLocaleString('zh-CN');
|
||
document.getElementById('redisConfigGroupName').value = `Redis配置_${timestamp}`;
|
||
document.getElementById('redisConfigGroupDescription').value = '';
|
||
|
||
new bootstrap.Modal(document.getElementById('saveRedisConfigModal')).show();
|
||
}
|
||
|
||
// 保存Redis配置组
|
||
async function saveRedisConfigGroup() {
|
||
const name = document.getElementById('redisConfigGroupName').value.trim();
|
||
const description = document.getElementById('redisConfigGroupDescription').value.trim();
|
||
|
||
if (!name) {
|
||
showAlert('请输入配置组名称', 'warning');
|
||
return;
|
||
}
|
||
|
||
const cluster1Config = getClusterConfig('cluster1');
|
||
const cluster2Config = getClusterConfig('cluster2');
|
||
const queryOptions = getQueryOptions();
|
||
|
||
if (!cluster1Config.nodes || cluster1Config.nodes.length === 0) {
|
||
showAlert('请配置集群1信息', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!cluster2Config.nodes || cluster2Config.nodes.length === 0) {
|
||
showAlert('请配置集群2信息', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/redis/config-groups', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
name: name,
|
||
description: description,
|
||
cluster1_config: cluster1Config,
|
||
cluster2_config: cluster2Config,
|
||
query_options: queryOptions
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showAlert(result.message, 'success');
|
||
bootstrap.Modal.getInstance(document.getElementById('saveRedisConfigModal')).hide();
|
||
loadRedisConfigGroups(); // 重新加载配置组列表
|
||
} else {
|
||
showAlert(result.error, 'danger');
|
||
}
|
||
} catch (error) {
|
||
showAlert(`保存失败: ${error.message}`, 'danger');
|
||
}
|
||
}
|
||
|
||
// 加载选定的Redis配置组
|
||
async function loadSelectedRedisConfigGroup() {
|
||
const groupId = document.getElementById('redisConfigGroupSelect').value;
|
||
|
||
if (!groupId) {
|
||
showAlert('请选择配置组', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/redis/config-groups/${groupId}`);
|
||
const result = await response.json();
|
||
|
||
if (result.success && result.data) {
|
||
const config = result.data;
|
||
|
||
// 加载集群1配置
|
||
loadClusterConfig('cluster1', config.cluster1_config);
|
||
|
||
// 加载集群2配置
|
||
loadClusterConfig('cluster2', config.cluster2_config);
|
||
|
||
// 加载查询选项
|
||
loadQueryOptions(config.query_options);
|
||
|
||
showAlert(`配置组 "${config.name}" 加载成功`, 'success');
|
||
} else {
|
||
showAlert(result.error, 'danger');
|
||
}
|
||
} catch (error) {
|
||
showAlert(`加载失败: ${error.message}`, 'danger');
|
||
}
|
||
}
|
||
|
||
// 加载集群配置到界面
|
||
function loadClusterConfig(clusterId, config) {
|
||
// 设置集群名称
|
||
document.getElementById(`${clusterId}-name`).value = config.name || '';
|
||
|
||
// 设置密码
|
||
document.getElementById(`${clusterId}-password`).value = config.password || '';
|
||
|
||
// 设置超时和连接数
|
||
document.getElementById(`${clusterId}-timeout`).value = config.socket_timeout || 3;
|
||
document.getElementById(`${clusterId}-max-conn`).value = config.max_connections_per_node || 16;
|
||
|
||
// 清空现有节点
|
||
const container = document.getElementById(`${clusterId}-nodes`);
|
||
container.innerHTML = '';
|
||
|
||
// 添加节点
|
||
if (config.nodes && config.nodes.length > 0) {
|
||
config.nodes.forEach(node => {
|
||
const nodeInput = document.createElement('div');
|
||
nodeInput.className = 'node-input';
|
||
nodeInput.innerHTML = `
|
||
<input type="text" class="form-control node-host" placeholder="主机地址" value="${node.host}">
|
||
<input type="number" class="form-control node-port" placeholder="端口" value="${node.port}" style="width: 100px;">
|
||
<button type="button" class="btn btn-outline-danger btn-sm remove-node">
|
||
<i class="fas fa-minus"></i>
|
||
</button>
|
||
`;
|
||
container.appendChild(nodeInput);
|
||
});
|
||
} else {
|
||
// 添加默认节点
|
||
addNode(clusterId);
|
||
}
|
||
|
||
updateNodeDeleteButtons();
|
||
}
|
||
|
||
// 加载查询选项
|
||
function loadQueryOptions(queryOptions) {
|
||
if (queryOptions.mode === 'random') {
|
||
document.getElementById('randomMode').checked = true;
|
||
document.getElementById('sampleCount').value = queryOptions.count || 100;
|
||
document.getElementById('keyPattern').value = queryOptions.pattern || '*';
|
||
document.getElementById('sourceCluster').value = queryOptions.source_cluster || 'cluster2';
|
||
} else {
|
||
document.getElementById('specifiedMode').checked = true;
|
||
document.getElementById('specifiedKeys').value = (queryOptions.keys || []).join('\n');
|
||
}
|
||
|
||
toggleQueryMode();
|
||
}
|
||
|
||
// 显示Redis配置管理对话框
|
||
function showManageRedisConfigDialog() {
|
||
loadRedisConfigGroupsForManagement();
|
||
new bootstrap.Modal(document.getElementById('manageRedisConfigModal')).show();
|
||
}
|
||
|
||
// 为管理界面加载Redis配置组
|
||
async function loadRedisConfigGroupsForManagement() {
|
||
try {
|
||
const response = await fetch('/api/redis/config-groups');
|
||
const result = await response.json();
|
||
|
||
const container = document.getElementById('redisConfigGroupList');
|
||
|
||
if (result.success && result.data && result.data.length > 0) {
|
||
let html = '<div class="table-responsive"><table class="table table-striped">';
|
||
html += '<thead><tr><th>名称</th><th>描述</th><th>创建时间</th><th>操作</th></tr></thead><tbody>';
|
||
|
||
result.data.forEach(group => {
|
||
html += `
|
||
<tr>
|
||
<td>${group.name}</td>
|
||
<td>${group.description || '无描述'}</td>
|
||
<td>${new Date(group.created_at).toLocaleString('zh-CN')}</td>
|
||
<td>
|
||
<button class="btn btn-primary btn-sm me-1" onclick="loadRedisConfigGroupById(${group.id})">
|
||
<i class="fas fa-download"></i> 加载
|
||
</button>
|
||
<button class="btn btn-danger btn-sm" onclick="deleteRedisConfigGroup(${group.id}, '${group.name}')">
|
||
<i class="fas fa-trash"></i> 删除
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += '</tbody></table></div>';
|
||
container.innerHTML = html;
|
||
} else {
|
||
container.innerHTML = '<div class="text-center text-muted py-4">暂无Redis配置组</div>';
|
||
}
|
||
} catch (error) {
|
||
document.getElementById('redisConfigGroupList').innerHTML = '<div class="alert alert-danger">加载失败: ' + error.message + '</div>';
|
||
}
|
||
}
|
||
|
||
// 通过ID加载Redis配置组
|
||
async function loadRedisConfigGroupById(groupId) {
|
||
try {
|
||
const response = await fetch(`/api/redis/config-groups/${groupId}`);
|
||
const result = await response.json();
|
||
|
||
if (result.success && result.data) {
|
||
const config = result.data;
|
||
|
||
// 加载配置
|
||
loadClusterConfig('cluster1', config.cluster1_config);
|
||
loadClusterConfig('cluster2', config.cluster2_config);
|
||
loadQueryOptions(config.query_options);
|
||
|
||
// 关闭管理对话框
|
||
bootstrap.Modal.getInstance(document.getElementById('manageRedisConfigModal')).hide();
|
||
|
||
showAlert(`配置组 "${config.name}" 加载成功`, 'success');
|
||
} else {
|
||
showAlert(result.error, 'danger');
|
||
}
|
||
} catch (error) {
|
||
showAlert(`加载失败: ${error.message}`, 'danger');
|
||
}
|
||
}
|
||
|
||
// 删除Redis配置组
|
||
async function deleteRedisConfigGroup(groupId, groupName) {
|
||
if (!confirm(`确定要删除配置组 "${groupName}" 吗?此操作不可恢复。`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/redis/config-groups/${groupId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showAlert(result.message, 'success');
|
||
loadRedisConfigGroupsForManagement(); // 重新加载列表
|
||
loadRedisConfigGroups(); // 重新加载下拉框
|
||
} else {
|
||
showAlert(result.error, 'danger');
|
||
}
|
||
} catch (error) {
|
||
showAlert(`删除失败: ${error.message}`, 'danger');
|
||
}
|
||
}
|
||
|
||
// 显示Redis配置导入对话框
|
||
function showImportRedisConfigDialog(targetCluster) {
|
||
currentImportTarget = targetCluster;
|
||
document.getElementById('redisConfigImportText').value = '';
|
||
new bootstrap.Modal(document.getElementById('importRedisConfigModal')).show();
|
||
}
|
||
|
||
// 导入Redis配置
|
||
async function importRedisConfig() {
|
||
const configText = document.getElementById('redisConfigImportText').value.trim();
|
||
|
||
if (!configText) {
|
||
showAlert('请输入配置内容', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/redis/import-config', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
config_text: configText
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success && result.data) {
|
||
// 将配置应用到目标集群
|
||
loadClusterConfig(currentImportTarget, result.data);
|
||
|
||
bootstrap.Modal.getInstance(document.getElementById('importRedisConfigModal')).hide();
|
||
showAlert(result.message, 'success');
|
||
} else {
|
||
showAlert(result.error, 'danger');
|
||
}
|
||
} catch (error) {
|
||
showAlert(`导入失败: ${error.message}`, 'danger');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Redis查询历史管理功能
|
||
*/
|
||
|
||
// 加载Redis查询历史
|
||
async function loadRedisQueryHistory() {
|
||
try {
|
||
const response = await fetch('/api/redis/query-history');
|
||
const result = await response.json();
|
||
|
||
const select = document.getElementById('redisHistorySelect');
|
||
select.innerHTML = '<option value="">选择历史记录...</option>';
|
||
|
||
if (result.success && result.data) {
|
||
result.data.forEach(history => {
|
||
const option = document.createElement('option');
|
||
option.value = history.id;
|
||
option.textContent = `${history.name} - ${new Date(history.created_at).toLocaleString('zh-CN')}`;
|
||
select.appendChild(option);
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('加载Redis查询历史失败:', error);
|
||
}
|
||
}
|
||
|
||
// 显示保存Redis查询历史对话框
|
||
function showSaveRedisHistoryDialog() {
|
||
if (!currentResults) {
|
||
showAlert('请先执行Redis比较查询', 'warning');
|
||
return;
|
||
}
|
||
|
||
// 生成默认名称
|
||
const timestamp = new Date().toLocaleString('zh-CN');
|
||
document.getElementById('redisHistoryName').value = `Redis查询_${timestamp}`;
|
||
document.getElementById('redisHistoryDescription').value = `Redis比较结果 - 总计${currentResults.stats.total_keys}个Key,发现${currentResults.stats.different_count}处差异`;
|
||
|
||
new bootstrap.Modal(document.getElementById('saveRedisHistoryModal')).show();
|
||
}
|
||
|
||
// 保存Redis查询历史
|
||
async function saveRedisQueryHistory() {
|
||
if (!currentResults) {
|
||
showAlert('没有可保存的查询结果', 'warning');
|
||
return;
|
||
}
|
||
|
||
const name = document.getElementById('redisHistoryName').value.trim();
|
||
const description = document.getElementById('redisHistoryDescription').value.trim();
|
||
|
||
if (!name) {
|
||
showAlert('请输入历史记录名称', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/redis/query-history', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
name: name,
|
||
description: description,
|
||
cluster1_config: getClusterConfig('cluster1'),
|
||
cluster2_config: getClusterConfig('cluster2'),
|
||
query_options: getQueryOptions(),
|
||
query_keys: currentResults.query_options?.keys || [],
|
||
results_summary: currentResults.stats,
|
||
execution_time: currentResults.performance_report?.total_time || 0,
|
||
total_keys: currentResults.stats.total_keys,
|
||
different_count: currentResults.stats.different_count,
|
||
identical_count: currentResults.stats.identical_count,
|
||
missing_count: currentResults.stats.missing_in_cluster1 + currentResults.stats.missing_in_cluster2,
|
||
raw_results: {
|
||
identical_results: currentResults.identical_results,
|
||
different_results: currentResults.different_results,
|
||
missing_results: currentResults.missing_results,
|
||
performance_report: currentResults.performance_report
|
||
}
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showAlert(result.message, 'success');
|
||
bootstrap.Modal.getInstance(document.getElementById('saveRedisHistoryModal')).hide();
|
||
loadRedisQueryHistory(); // 重新加载历史记录列表
|
||
} else {
|
||
showAlert(result.error, 'danger');
|
||
}
|
||
} catch (error) {
|
||
showAlert(`保存失败: ${error.message}`, 'danger');
|
||
}
|
||
}
|
||
|
||
// 加载选定的Redis查询历史
|
||
async function loadSelectedRedisHistory() {
|
||
const historyId = document.getElementById('redisHistorySelect').value;
|
||
|
||
if (!historyId) {
|
||
showAlert('请选择历史记录', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/redis/query-history/${historyId}`);
|
||
const result = await response.json();
|
||
|
||
if (result.success && result.data) {
|
||
const history = result.data;
|
||
|
||
// 加载配置
|
||
loadClusterConfig('cluster1', history.cluster1_config);
|
||
loadClusterConfig('cluster2', history.cluster2_config);
|
||
loadQueryOptions(history.query_options);
|
||
|
||
// 如果有原始结果,直接显示
|
||
if (history.raw_results) {
|
||
const displayResults = {
|
||
stats: history.results_summary,
|
||
identical_results: history.raw_results.identical_results || [],
|
||
different_results: history.raw_results.different_results || [],
|
||
missing_results: history.raw_results.missing_results || [],
|
||
performance_report: history.raw_results.performance_report || {},
|
||
clusters: {
|
||
cluster1_name: history.cluster1_config.name,
|
||
cluster2_name: history.cluster2_config.name
|
||
}
|
||
};
|
||
|
||
currentResults = displayResults;
|
||
displayResults(displayResults);
|
||
showAlert(`历史记录 "${history.name}" 加载成功`, 'success');
|
||
} else {
|
||
showAlert(`历史记录 "${history.name}" 配置加载成功,但没有结果数据`, 'info');
|
||
}
|
||
} else {
|
||
showAlert(result.error, 'danger');
|
||
}
|
||
} catch (error) {
|
||
showAlert(`加载失败: ${error.message}`, 'danger');
|
||
}
|
||
}
|
||
|
||
// 显示Redis查询历史管理对话框
|
||
function showManageRedisHistoryDialog() {
|
||
loadRedisHistoryForManagement();
|
||
new bootstrap.Modal(document.getElementById('manageRedisHistoryModal')).show();
|
||
}
|
||
|
||
// 为管理界面加载Redis查询历史
|
||
async function loadRedisHistoryForManagement() {
|
||
try {
|
||
const response = await fetch('/api/redis/query-history');
|
||
const result = await response.json();
|
||
|
||
const container = document.getElementById('redisHistoryList');
|
||
|
||
if (result.success && result.data && result.data.length > 0) {
|
||
let html = '<div class="table-responsive"><table class="table table-striped">';
|
||
html += '<thead><tr><th>名称</th><th>描述</th><th>Key数量</th><th>差异数</th><th>执行时间</th><th>创建时间</th><th>操作</th></tr></thead><tbody>';
|
||
|
||
result.data.forEach(history => {
|
||
html += `
|
||
<tr>
|
||
<td>${history.name}</td>
|
||
<td>${history.description || '无描述'}</td>
|
||
<td>${history.total_keys}</td>
|
||
<td>${history.different_count}</td>
|
||
<td>${history.execution_time.toFixed(3)}s</td>
|
||
<td>${new Date(history.created_at).toLocaleString('zh-CN')}</td>
|
||
<td>
|
||
<button class="btn btn-warning btn-sm me-1" onclick="loadRedisHistoryById(${history.id})">
|
||
<i class="fas fa-history"></i> 加载
|
||
</button>
|
||
<button class="btn btn-danger btn-sm" onclick="deleteRedisHistory(${history.id}, '${history.name}')">
|
||
<i class="fas fa-trash"></i> 删除
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
html += '</tbody></table></div>';
|
||
container.innerHTML = html;
|
||
} else {
|
||
container.innerHTML = '<div class="text-center text-muted py-4">暂无Redis查询历史</div>';
|
||
}
|
||
} catch (error) {
|
||
document.getElementById('redisHistoryList').innerHTML = '<div class="alert alert-danger">加载失败: ' + error.message + '</div>';
|
||
}
|
||
}
|
||
|
||
// 通过ID加载Redis查询历史
|
||
async function loadRedisHistoryById(historyId) {
|
||
try {
|
||
const response = await fetch(`/api/redis/query-history/${historyId}`);
|
||
const result = await response.json();
|
||
|
||
if (result.success && result.data) {
|
||
const history = result.data;
|
||
|
||
// 加载配置
|
||
loadClusterConfig('cluster1', history.cluster1_config);
|
||
loadClusterConfig('cluster2', history.cluster2_config);
|
||
loadQueryOptions(history.query_options);
|
||
|
||
// 如果有原始结果,直接显示
|
||
if (history.raw_results) {
|
||
const displayResults = {
|
||
stats: history.results_summary,
|
||
identical_results: history.raw_results.identical_results || [],
|
||
different_results: history.raw_results.different_results || [],
|
||
missing_results: history.raw_results.missing_results || [],
|
||
performance_report: history.raw_results.performance_report || {},
|
||
clusters: {
|
||
cluster1_name: history.cluster1_config.name,
|
||
cluster2_name: history.cluster2_config.name
|
||
}
|
||
};
|
||
|
||
currentResults = displayResults;
|
||
displayResults(displayResults);
|
||
}
|
||
|
||
// 关闭管理对话框
|
||
bootstrap.Modal.getInstance(document.getElementById('manageRedisHistoryModal')).hide();
|
||
|
||
showAlert(`历史记录 "${history.name}" 加载成功`, 'success');
|
||
} else {
|
||
showAlert(result.error, 'danger');
|
||
}
|
||
} catch (error) {
|
||
showAlert(`加载失败: ${error.message}`, 'danger');
|
||
}
|
||
}
|
||
|
||
// 删除Redis查询历史
|
||
async function deleteRedisHistory(historyId, historyName) {
|
||
if (!confirm(`确定要删除历史记录 "${historyName}" 吗?此操作不可恢复。`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/redis/query-history/${historyId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showAlert(result.message, 'success');
|
||
loadRedisHistoryForManagement(); // 重新加载列表
|
||
loadRedisQueryHistory(); // 重新加载下拉框
|
||
} else {
|
||
showAlert(result.error, 'danger');
|
||
}
|
||
} catch (error) {
|
||
showAlert(`删除失败: ${error.message}`, 'danger');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Redis查询日志功能
|
||
*/
|
||
|
||
// 显示Redis查询日志对话框
|
||
function showRedisQueryLogsDialog() {
|
||
loadRedisQueryLogs();
|
||
new bootstrap.Modal(document.getElementById('redisQueryLogsModal')).show();
|
||
}
|
||
|
||
// 加载Redis查询日志
|
||
async function loadRedisQueryLogs() {
|
||
try {
|
||
const response = await fetch('/api/query-logs?limit=1000');
|
||
const result = await response.json();
|
||
|
||
const container = document.getElementById('redisQueryLogs');
|
||
|
||
if (result.success && result.data && result.data.length > 0) {
|
||
// 过滤Redis相关的日志
|
||
const redisLogs = result.data.filter(log =>
|
||
log.message.toLowerCase().includes('redis') ||
|
||
log.query_type === 'redis'
|
||
);
|
||
|
||
if (redisLogs.length > 0) {
|
||
let html = '';
|
||
redisLogs.forEach(log => {
|
||
const levelClass = log.level === 'ERROR' ? 'text-danger' :
|
||
log.level === 'WARNING' ? 'text-warning' : 'text-info';
|
||
html += `
|
||
<div class="mb-2">
|
||
<span class="text-muted">[${log.timestamp}]</span>
|
||
<span class="badge bg-secondary">${log.level}</span>
|
||
<span class="${levelClass}">${log.message}</span>
|
||
</div>
|
||
`;
|
||
});
|
||
container.innerHTML = html;
|
||
} else {
|
||
container.innerHTML = '<div class="text-center text-muted py-4">暂无Redis查询日志</div>';
|
||
}
|
||
} else {
|
||
container.innerHTML = '<div class="text-center text-muted py-4">暂无查询日志</div>';
|
||
}
|
||
} catch (error) {
|
||
document.getElementById('redisQueryLogs').innerHTML = '<div class="alert alert-danger">加载日志失败: ' + error.message + '</div>';
|
||
}
|
||
}
|
||
|
||
// 刷新Redis查询日志
|
||
function refreshRedisQueryLogs() {
|
||
loadRedisQueryLogs();
|
||
showAlert('查询日志已刷新', 'info');
|
||
}
|
||
|
||
// 清空Redis查询日志
|
||
async function clearRedisQueryLogs() {
|
||
if (!confirm('确定要清空所有查询日志吗?此操作不可恢复。')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/query-logs', {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showAlert(result.message, 'success');
|
||
loadRedisQueryLogs(); // 重新加载日志
|
||
} else {
|
||
showAlert(result.error, 'danger');
|
||
}
|
||
} catch (error) {
|
||
showAlert(`清空日志失败: ${error.message}`, 'danger');
|
||
}
|
||
} |