feat: initial commit - ServerManager Pro v2.0.0
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const BatchOperations = () => {
|
||||
const [servers, setServers] = useState([]);
|
||||
const [selectedServers, setSelectedServers] = useState([]);
|
||||
const [command, setCommand] = useState('');
|
||||
const [results, setResults] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [commandHistory, setCommandHistory] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadServers();
|
||||
loadCommandHistory();
|
||||
}, []);
|
||||
|
||||
const loadServers = async () => {
|
||||
if (window.electronAPI) {
|
||||
const result = await window.electronAPI.loadServers();
|
||||
if (result.success) {
|
||||
setServers(result.servers);
|
||||
// По умолчанию выбираем все серверы
|
||||
setSelectedServers(result.servers.map(s => s.id));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadCommandHistory = async () => {
|
||||
// Загружаем историю команд из localStorage
|
||||
const history = localStorage.getItem('batchCommandHistory');
|
||||
if (history) {
|
||||
setCommandHistory(JSON.parse(history));
|
||||
}
|
||||
};
|
||||
|
||||
const saveCommandToHistory = (cmd) => {
|
||||
const newHistory = [cmd, ...commandHistory.filter(c => c !== cmd)].slice(0, 10);
|
||||
setCommandHistory(newHistory);
|
||||
localStorage.setItem('batchCommandHistory', JSON.stringify(newHistory));
|
||||
};
|
||||
|
||||
const executeBatchCommand = async () => {
|
||||
if (!command.trim()) {
|
||||
alert('Введите команду для выполнения');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedServers.length === 0) {
|
||||
alert('Выберите хотя бы один сервер');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setResults([]);
|
||||
|
||||
const selectedServerObjects = servers.filter(s => selectedServers.includes(s.id));
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.executeBatchCommand(selectedServerObjects, command);
|
||||
|
||||
if (result.success) {
|
||||
setResults(result.results);
|
||||
saveCommandToHistory(command);
|
||||
} else {
|
||||
setResults([{ server: 'System', success: false, error: 'Failed to execute batch command' }]);
|
||||
}
|
||||
} catch (error) {
|
||||
setResults([{ server: 'System', success: false, error: error.message }]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleServerSelection = (serverId) => {
|
||||
setSelectedServers(prev =>
|
||||
prev.includes(serverId)
|
||||
? prev.filter(id => id !== serverId)
|
||||
: [...prev, serverId]
|
||||
);
|
||||
};
|
||||
|
||||
const selectAllServers = () => {
|
||||
setSelectedServers(servers.map(s => s.id));
|
||||
};
|
||||
|
||||
const deselectAllServers = () => {
|
||||
setSelectedServers([]);
|
||||
};
|
||||
|
||||
const getSuccessCount = () => results.filter(r => r.success).length;
|
||||
const getTotalCount = () => results.length;
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-white">Пакетные операции</h1>
|
||||
<p className="text-gray-400">Массовое выполнение команд на нескольких серверах</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Левая колонка - выбор серверов */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-gray-800 rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="font-semibold text-white">Серверы</h3>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={selectAllServers}
|
||||
className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700"
|
||||
>
|
||||
Все
|
||||
</button>
|
||||
<button
|
||||
onClick={deselectAllServers}
|
||||
className="px-3 py-1 bg-gray-600 text-white rounded text-sm hover:bg-gray-700"
|
||||
>
|
||||
Ничего
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{servers.map(server => (
|
||||
<label key={server.id} className="flex items-center gap-3 p-2 hover:bg-gray-700 rounded cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedServers.includes(server.id)}
|
||||
onChange={() => toggleServerSelection(server.id)}
|
||||
className="rounded border-gray-600 bg-gray-700"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-white font-medium">{server.name}</div>
|
||||
<div className="text-gray-400 text-sm">{server.ip}</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-3 bg-gray-700 rounded text-sm text-gray-300">
|
||||
Выбрано: {selectedServers.length} из {servers.length} серверов
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* История команд */}
|
||||
{commandHistory.length > 0 && (
|
||||
<div className="bg-gray-800 rounded-lg p-4 mt-4">
|
||||
<h3 className="font-semibold text-white mb-3">История команд</h3>
|
||||
<div className="space-y-2">
|
||||
{commandHistory.map((cmd, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setCommand(cmd)}
|
||||
className="w-full text-left p-2 text-sm bg-gray-700 rounded hover:bg-gray-600 text-gray-300 font-mono truncate"
|
||||
title={cmd}
|
||||
>
|
||||
{cmd}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Правая колонка - команда и результаты */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-3">Команда для выполнения</h3>
|
||||
<textarea
|
||||
value={command}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
placeholder="Введите команду для выполнения на выбранных серверах..."
|
||||
rows="4"
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-3 text-white placeholder-gray-400 focus:border-blue-500 focus:outline-none font-mono text-sm"
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mt-3">
|
||||
<div className="text-gray-400 text-sm">
|
||||
Примеры: df -h, uptime, systemctl status nginx
|
||||
</div>
|
||||
<button
|
||||
onClick={executeBatchCommand}
|
||||
disabled={loading || selectedServers.length === 0}
|
||||
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{loading ? '🔄' : '⚡'}
|
||||
{loading ? 'Выполнение...' : `Выполнить (${selectedServers.length})`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{results.length > 0 && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold text-white">Результаты</h3>
|
||||
<div className={`px-3 py-1 rounded text-sm font-medium ${
|
||||
getSuccessCount() === getTotalCount()
|
||||
? 'bg-green-600 text-white'
|
||||
: getSuccessCount() > 0
|
||||
? 'bg-yellow-600 text-white'
|
||||
: 'bg-red-600 text-white'
|
||||
}`}>
|
||||
{getSuccessCount()}/{getTotalCount()} успешно
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{results.map((result, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-4 rounded-lg border ${
|
||||
result.success
|
||||
? 'bg-green-900 border-green-600'
|
||||
: 'bg-red-900 border-red-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<span className="font-semibold text-white">{result.server}</span>
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
result.success ? 'bg-green-600 text-white' : 'bg-red-600 text-white'
|
||||
}`}>
|
||||
{result.success ? 'УСПЕХ' : 'ОШИБКА'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{result.success ? (
|
||||
<pre className="text-green-200 text-sm font-mono whitespace-pre-wrap">
|
||||
{result.output}
|
||||
</pre>
|
||||
) : (
|
||||
<div className="text-red-200 text-sm">
|
||||
{result.error || result.output}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BatchOperations;
|
||||
@@ -0,0 +1,369 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const CommandTemplates = () => {
|
||||
const [templates, setTemplates] = useState([]);
|
||||
const [servers, setServers] = useState([]);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [selectedServer, setSelectedServer] = useState('');
|
||||
const [selectedTemplate, setSelectedTemplate] = useState('');
|
||||
const [commandOutput, setCommandOutput] = useState('');
|
||||
const [newTemplate, setNewTemplate] = useState({
|
||||
name: '',
|
||||
command: '',
|
||||
description: '',
|
||||
category: 'system'
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
loadServers();
|
||||
}, []);
|
||||
|
||||
const loadTemplates = async () => {
|
||||
if (window.electronAPI) {
|
||||
const result = await window.electronAPI.loadCommandTemplates();
|
||||
if (result.success) {
|
||||
setTemplates(result.templates || []);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadServers = async () => {
|
||||
if (window.electronAPI) {
|
||||
const result = await window.electronAPI.loadServers();
|
||||
if (result.success) {
|
||||
setServers(result.servers || []);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const saveTemplates = async (templatesList) => {
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.saveCommandTemplates(templatesList);
|
||||
}
|
||||
};
|
||||
|
||||
const addTemplate = async () => {
|
||||
if (!newTemplate.name || !newTemplate.command) {
|
||||
alert('Заполните название и команду');
|
||||
return;
|
||||
}
|
||||
|
||||
const template = {
|
||||
...newTemplate,
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const updatedTemplates = [...templates, template];
|
||||
setTemplates(updatedTemplates);
|
||||
await saveTemplates(updatedTemplates);
|
||||
setShowAddForm(false);
|
||||
setNewTemplate({
|
||||
name: '',
|
||||
command: '',
|
||||
description: '',
|
||||
category: 'system'
|
||||
});
|
||||
};
|
||||
|
||||
const deleteTemplate = async (templateId) => {
|
||||
if (confirm('Удалить шаблон?')) {
|
||||
const updatedTemplates = templates.filter(t => t.id !== templateId);
|
||||
setTemplates(updatedTemplates);
|
||||
await saveTemplates(updatedTemplates);
|
||||
}
|
||||
};
|
||||
|
||||
const executeCommand = async () => {
|
||||
if (!selectedServer || !selectedTemplate) {
|
||||
alert('Выберите сервер и шаблон команды');
|
||||
return;
|
||||
}
|
||||
|
||||
const server = servers.find(s => s.id === selectedServer);
|
||||
const template = templates.find(t => t.id === selectedTemplate);
|
||||
|
||||
if (!server || !template) return;
|
||||
|
||||
setCommandOutput('Выполнение команды...');
|
||||
|
||||
// Имитация выполнения команды
|
||||
setTimeout(() => {
|
||||
setCommandOutput(`Команда выполнена на сервере ${server.name}\n\nКоманда: ${template.command}\n\nРезультат: Команда успешно выполнена\nВремя выполнения: 0.5с`);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const scrollToExecuteSection = () => {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById('execute-section');
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const getCategoryColor = (category) => {
|
||||
const colors = {
|
||||
system: 'bg-blue-500 text-white',
|
||||
network: 'bg-green-500 text-white',
|
||||
security: 'bg-red-500 text-white',
|
||||
monitoring: 'bg-purple-500 text-white',
|
||||
other: 'bg-gray-500 text-white'
|
||||
};
|
||||
return colors[category] || colors.other;
|
||||
};
|
||||
|
||||
const predefinedTemplates = [
|
||||
{
|
||||
name: 'Проверка диска',
|
||||
command: 'df -h',
|
||||
description: 'Показать использование дискового пространства',
|
||||
category: 'system'
|
||||
},
|
||||
{
|
||||
name: 'Загрузка системы',
|
||||
command: 'uptime',
|
||||
description: 'Показать время работы и нагрузку системы',
|
||||
category: 'system'
|
||||
},
|
||||
{
|
||||
name: 'Сетевые соединения',
|
||||
command: 'netstat -tulpn',
|
||||
description: 'Показать активные сетевые соединения',
|
||||
category: 'network'
|
||||
},
|
||||
{
|
||||
name: 'Процессы',
|
||||
command: 'top -bn1 | head -20',
|
||||
description: 'Показать топ процессов по использованию ресурсов',
|
||||
category: 'monitoring'
|
||||
}
|
||||
];
|
||||
|
||||
const addPredefinedTemplate = (template) => {
|
||||
const newTemplate = {
|
||||
...template,
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
const updatedTemplates = [...templates, newTemplate];
|
||||
setTemplates(updatedTemplates);
|
||||
saveTemplates(updatedTemplates);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 md:mb-6 gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl md:text-2xl font-bold text-white">Шаблоны команд</h1>
|
||||
<p className="text-gray-400 text-sm md:text-base">Быстрое выполнение часто используемых команд</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 flex items-center space-x-2 w-full md:w-auto justify-center"
|
||||
>
|
||||
<span>+</span>
|
||||
<span>Добавить шаблон</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Быстрое выполнение */}
|
||||
<div id="execute-section" className="bg-gray-800 rounded-lg shadow-lg p-4 md:p-6 mb-4 md:mb-6 border border-gray-700">
|
||||
<h3 className="text-lg font-semibold mb-4 text-white">Быстрое выполнение</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Сервер</label>
|
||||
<select
|
||||
value={selectedServer}
|
||||
onChange={(e) => setSelectedServer(e.target.value)}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">Выберите сервер</option>
|
||||
{servers.map(server => (
|
||||
<option key={server.id} value={server.id}>{server.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Шаблон команды</label>
|
||||
<select
|
||||
value={selectedTemplate}
|
||||
onChange={(e) => setSelectedTemplate(e.target.value)}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">Выберите шаблон</option>
|
||||
{templates.map(template => (
|
||||
<option key={template.id} value={template.id}>{template.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<button
|
||||
onClick={executeCommand}
|
||||
disabled={!selectedServer || !selectedTemplate}
|
||||
className="w-full px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Выполнить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{commandOutput && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Результат:</label>
|
||||
<pre className="bg-gray-900 p-4 rounded-lg text-sm font-mono whitespace-pre-wrap border border-gray-700 text-white max-h-60 overflow-y-auto">
|
||||
{commandOutput}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Предопределенные шаблоны */}
|
||||
<div className="bg-gray-800 rounded-lg shadow-lg p-4 md:p-6 mb-4 md:mb-6 border border-gray-700">
|
||||
<h3 className="text-lg font-semibold mb-4 text-white">Готовые шаблоны</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{predefinedTemplates.map((template, index) => (
|
||||
<div key={index} className="border border-gray-700 rounded-lg p-4 bg-gray-750">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h4 className="font-semibold text-white text-base md:text-lg">{template.name}</h4>
|
||||
<span className={`px-2 py-1 rounded text-xs ${getCategoryColor(template.category)}`}>
|
||||
{template.category}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mb-3">{template.description}</p>
|
||||
<code className="block bg-gray-900 p-2 rounded text-sm font-mono mb-3 text-white">
|
||||
{template.command}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => addPredefinedTemplate(template)}
|
||||
className="w-full px-3 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 text-sm"
|
||||
>
|
||||
Добавить в мои шаблоны
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Форма добавления шаблона */}
|
||||
{showAddForm && (
|
||||
<div className="bg-gray-800 rounded-lg shadow-lg p-4 md:p-6 mb-4 md:mb-6 border border-gray-700">
|
||||
<h3 className="text-lg font-semibold mb-4 text-white">Добавить шаблон команды</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Название</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newTemplate.name}
|
||||
onChange={(e) => setNewTemplate({...newTemplate, name: e.target.value})}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
placeholder="Моя команда"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Команда</label>
|
||||
<textarea
|
||||
value={newTemplate.command}
|
||||
onChange={(e) => setNewTemplate({...newTemplate, command: e.target.value})}
|
||||
rows="3"
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent font-mono"
|
||||
placeholder="Введите команду для выполнения на сервере"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Описание</label>
|
||||
<textarea
|
||||
value={newTemplate.description}
|
||||
onChange={(e) => setNewTemplate({...newTemplate, description: e.target.value})}
|
||||
rows="2"
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
placeholder="Описание команды"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">Категория</label>
|
||||
<select
|
||||
value={newTemplate.category}
|
||||
onChange={(e) => setNewTemplate({...newTemplate, category: e.target.value})}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
>
|
||||
<option value="system">Система</option>
|
||||
<option value="network">Сеть</option>
|
||||
<option value="security">Безопасность</option>
|
||||
<option value="monitoring">Мониторинг</option>
|
||||
<option value="other">Другое</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row justify-end space-y-2 md:space-y-0 md:space-x-3">
|
||||
<button
|
||||
onClick={() => setShowAddForm(false)}
|
||||
className="px-4 py-2 border border-gray-600 text-gray-300 rounded-lg hover:bg-gray-700 w-full md:w-auto"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
onClick={addTemplate}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 w-full md:w-auto"
|
||||
>
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Мои шаблоны */}
|
||||
<div className="bg-gray-800 rounded-lg shadow-lg p-4 md:p-6 border border-gray-700">
|
||||
<h3 className="text-lg font-semibold mb-4 text-white">Мои шаблоны команд</h3>
|
||||
{templates.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<div className="text-4xl mb-2">⚡</div>
|
||||
<p>Нет сохраненных шаблонов команд</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{templates.map(template => (
|
||||
<div key={template.id} className="border border-gray-700 rounded-lg p-4 bg-gray-750">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h4 className="font-semibold text-white text-base md:text-lg">{template.name}</h4>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedTemplate(template.id);
|
||||
scrollToExecuteSection();
|
||||
}}
|
||||
className="text-green-500 hover:text-green-400"
|
||||
title="Выполнить"
|
||||
>
|
||||
▶️
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteTemplate(template.id)}
|
||||
className="text-red-500 hover:text-red-400"
|
||||
title="Удалить"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mb-2">{template.description}</p>
|
||||
<code className="block bg-gray-900 p-2 rounded text-sm font-mono mb-2 text-white">
|
||||
{template.command}
|
||||
</code>
|
||||
<div className="flex justify-between items-center text-xs text-gray-500">
|
||||
<span className={`px-2 py-1 rounded ${getCategoryColor(template.category)}`}>
|
||||
{template.category}
|
||||
</span>
|
||||
<span>Создано: {new Date(template.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommandTemplates;
|
||||
@@ -0,0 +1,275 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [stats, setStats] = useState({
|
||||
totalServers: 0,
|
||||
onlineServers: 0,
|
||||
totalPasswords: 0,
|
||||
totalTemplates: 0,
|
||||
totalNotes: 0,
|
||||
recentActivity: [],
|
||||
systemStatus: {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
disk: 0
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboardData();
|
||||
const interval = setInterval(loadDashboardData, 30000); // Обновление каждые 30 секунд
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
if (window.electronAPI) {
|
||||
try {
|
||||
const servers = await window.electronAPI.loadServers();
|
||||
const passwords = await window.electronAPI.loadPasswords();
|
||||
const templates = await window.electronAPI.loadCommandTemplates();
|
||||
const notes = await window.electronAPI.loadNotes();
|
||||
|
||||
let serverList = [];
|
||||
let onlineCount = 0;
|
||||
|
||||
if (servers.success) {
|
||||
serverList = servers.servers || [];
|
||||
// Проверяем статус серверов
|
||||
for (const server of serverList) {
|
||||
const pingResult = await window.electronAPI.pingServer(server);
|
||||
if (pingResult.success && pingResult.online) {
|
||||
onlineCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем реальные данные о системе
|
||||
const systemData = await getSystemInfo();
|
||||
|
||||
// Формируем реальную активность
|
||||
const activities = await getRecentActivity();
|
||||
|
||||
setStats({
|
||||
totalServers: serverList.length,
|
||||
onlineServers: onlineCount,
|
||||
totalPasswords: passwords.success ? (passwords.passwords?.length || 0) : 0,
|
||||
totalTemplates: templates.success ? (templates.templates?.length || 0) : 0,
|
||||
totalNotes: notes.success ? (notes.notes?.length || 0) : 0,
|
||||
recentActivity: activities,
|
||||
systemStatus: systemData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard data:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getSystemInfo = async () => {
|
||||
// В реальном приложении здесь был бы вызов к systeminformation
|
||||
// Пока используем заглушку
|
||||
return {
|
||||
cpu: Math.floor(Math.random() * 30) + 10, // 10-40%
|
||||
memory: Math.floor(Math.random() * 50) + 30, // 30-80%
|
||||
disk: Math.floor(Math.random() * 40) + 20 // 20-60%
|
||||
};
|
||||
};
|
||||
|
||||
const getRecentActivity = async () => {
|
||||
const activities = [];
|
||||
const now = new Date();
|
||||
|
||||
// Получаем последние серверы
|
||||
const servers = await window.electronAPI.loadServers();
|
||||
if (servers.success && servers.servers) {
|
||||
const recentServers = servers.servers
|
||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||
.slice(0, 2);
|
||||
|
||||
recentServers.forEach(server => {
|
||||
activities.push({
|
||||
id: `server-${server.id}`,
|
||||
action: `Добавлен сервер: ${server.name}`,
|
||||
timestamp: server.createdAt,
|
||||
type: 'server'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем последние заметки
|
||||
const notes = await window.electronAPI.loadNotes();
|
||||
if (notes.success && notes.notes) {
|
||||
const recentNotes = notes.notes
|
||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
||||
.slice(0, 2);
|
||||
|
||||
recentNotes.forEach(note => {
|
||||
activities.push({
|
||||
id: `note-${note.id}`,
|
||||
action: `Создана заметка: ${note.title}`,
|
||||
timestamp: note.createdAt,
|
||||
type: 'note'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Сортируем по времени и берем последние 4
|
||||
return activities
|
||||
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
|
||||
.slice(0, 4);
|
||||
};
|
||||
|
||||
const getActivityIcon = (type) => {
|
||||
switch (type) {
|
||||
case 'server': return '🖥️';
|
||||
case 'note': return '📝';
|
||||
case 'password': return '🔐';
|
||||
case 'template': return '⚡';
|
||||
default: return '📌';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (value) => {
|
||||
if (value < 70) return 'text-green-500';
|
||||
if (value < 90) return 'text-yellow-500';
|
||||
return 'text-red-500';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="mb-4 md:mb-6">
|
||||
<h1 className="text-xl md:text-2xl font-bold text-white">Дашборд</h1>
|
||||
<p className="text-gray-400 text-sm md:text-base">Обзор состояния системы</p>
|
||||
</div>
|
||||
|
||||
{/* Статистика */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4 mb-4 md:mb-6">
|
||||
<div className="bg-gray-800 p-4 rounded-lg text-center">
|
||||
<div className="text-2xl md:text-3xl font-bold text-blue-500">{stats.totalServers}</div>
|
||||
<div className="text-xs md:text-sm text-gray-400">Всего серверов</div>
|
||||
<div className="text-xs text-green-500 mt-1">{stats.onlineServers} онлайн</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-4 rounded-lg text-center">
|
||||
<div className="text-2xl md:text-3xl font-bold text-purple-500">{stats.totalPasswords}</div>
|
||||
<div className="text-xs md:text-sm text-gray-400">Сохранено паролей</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-4 rounded-lg text-center">
|
||||
<div className="text-2xl md:text-3xl font-bold text-yellow-500">{stats.totalTemplates}</div>
|
||||
<div className="text-xs md:text-sm text-gray-400">Шаблонов команд</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-4 rounded-lg text-center">
|
||||
<div className="text-2xl md:text-3xl font-bold text-green-500">{stats.totalNotes}</div>
|
||||
<div className="text-xs md:text-sm text-gray-400">Заметок</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 md:gap-6">
|
||||
{/* Системный статус */}
|
||||
<div className="bg-gray-800 rounded-lg p-4 md:p-6 lg:col-span-1">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Системный статус</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-gray-400">CPU</span>
|
||||
<span className={getStatusColor(stats.systemStatus.cpu)}>
|
||||
{stats.systemStatus.cpu}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full transition-all duration-500"
|
||||
style={{ width: `${stats.systemStatus.cpu}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-gray-400">Память</span>
|
||||
<span className={getStatusColor(stats.systemStatus.memory)}>
|
||||
{stats.systemStatus.memory}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className="bg-green-500 h-2 rounded-full transition-all duration-500"
|
||||
style={{ width: `${stats.systemStatus.memory}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-gray-400">Диск</span>
|
||||
<span className={getStatusColor(stats.systemStatus.disk)}>
|
||||
{stats.systemStatus.disk}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className="bg-purple-500 h-2 rounded-full transition-all duration-500"
|
||||
style={{ width: `${stats.systemStatus.disk}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Недавняя активность */}
|
||||
<div className="bg-gray-800 rounded-lg p-4 md:p-6 lg:col-span-2">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Недавняя активность</h3>
|
||||
<div className="space-y-3">
|
||||
{stats.recentActivity.length > 0 ? (
|
||||
stats.recentActivity.map(activity => (
|
||||
<div key={activity.id} className="border-l-2 border-primary-500 pl-3 py-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm">{getActivityIcon(activity.type)}</span>
|
||||
<p className="text-white text-sm flex-1">{activity.action}</p>
|
||||
</div>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
{new Date(activity.timestamp).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center py-4 text-gray-400">
|
||||
<div className="text-2xl mb-2">📊</div>
|
||||
<p>Активности пока нет</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Быстрые действия */}
|
||||
<div className="mt-4 md:mt-6 bg-gray-800 rounded-lg p-4 md:p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Быстрые действия</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<button
|
||||
onClick={() => window.location.hash = 'servers'}
|
||||
className="p-3 bg-primary-600 rounded-lg hover:bg-primary-700 transition-colors text-white text-sm"
|
||||
>
|
||||
🖥️ Добавить сервер
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.hash = 'notes'}
|
||||
className="p-3 bg-green-600 rounded-lg hover:bg-green-700 transition-colors text-white text-sm"
|
||||
>
|
||||
📝 Новая заметка
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.hash = 'templates'}
|
||||
className="p-3 bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors text-white text-sm"
|
||||
>
|
||||
⚡ Шаблоны команд
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.location.hash = 'passwords'}
|
||||
className="p-3 bg-purple-600 rounded-lg hover:bg-purple-700 transition-colors text-white text-sm"
|
||||
>
|
||||
🔐 Менеджер паролей
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,206 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const NetworkMonitor = () => {
|
||||
const [networkAdapters, setNetworkAdapters] = useState([]);
|
||||
const [vpnConnections, setVpnConnections] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastUpdate, setLastUpdate] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadNetworkData();
|
||||
const interval = setInterval(loadNetworkData, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Fallback данные
|
||||
const getFallbackAdapters = () => [
|
||||
{
|
||||
name: 'eth0',
|
||||
description: 'Ethernet Interface',
|
||||
status: 'Up',
|
||||
ipAddress: '192.168.1.100',
|
||||
macAddress: '00:1B:44:11:3A:B7',
|
||||
type: 'Ethernet'
|
||||
}
|
||||
];
|
||||
|
||||
const loadNetworkData = async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Получаем реальные данные из Electron
|
||||
if (window.electronAPI) {
|
||||
const adaptersResult = await window.electronAPI.getNetworkAdapters();
|
||||
const vpnResult = await window.electronAPI.getVpnConnections();
|
||||
|
||||
if (adaptersResult.success) {
|
||||
setNetworkAdapters(adaptersResult.adapters || getFallbackAdapters());
|
||||
} else {
|
||||
setNetworkAdapters(getFallbackAdapters());
|
||||
}
|
||||
|
||||
if (vpnResult.success) {
|
||||
setVpnConnections(vpnResult.connections || []);
|
||||
} else {
|
||||
setVpnConnections([]);
|
||||
}
|
||||
} else {
|
||||
// Fallback данные если API недоступно
|
||||
setNetworkAdapters(getFallbackAdapters());
|
||||
setVpnConnections([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading network data:', error);
|
||||
setNetworkAdapters(getFallbackAdapters());
|
||||
setVpnConnections([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLastUpdate(new Date());
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
return status === 'Up' || status === 'Connected'
|
||||
? 'bg-green-500'
|
||||
: 'bg-red-500';
|
||||
};
|
||||
|
||||
const getAdapterIcon = (type) => {
|
||||
switch (type) {
|
||||
case 'Ethernet': return '🔌';
|
||||
case 'Wi-Fi': return '📶';
|
||||
case 'VPN': return '🛡️';
|
||||
case 'Loopback': return '🔄';
|
||||
default: return '🌐';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 md:mb-6 gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl md:text-2xl font-bold text-white">Мониторинг сети</h1>
|
||||
<p className="text-gray-400 text-sm md:text-base">
|
||||
{lastUpdate ? `Обновлено: ${lastUpdate.toLocaleTimeString()}` : 'Загрузка...'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={loadNetworkData}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50 w-full md:w-auto"
|
||||
>
|
||||
{loading ? 'Загрузка...' : 'Обновить'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Статистика */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-4 mb-4 md:mb-6">
|
||||
<div className="bg-gray-800 p-3 md:p-4 rounded-lg text-center">
|
||||
<div className="text-lg md:text-2xl font-bold text-white">{networkAdapters.length}</div>
|
||||
<div className="text-xs md:text-sm text-gray-400">Интерфейсы</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-3 md:p-4 rounded-lg text-center">
|
||||
<div className="text-lg md:text-2xl font-bold text-green-500">
|
||||
{networkAdapters.filter(a => a.status === 'Up').length}
|
||||
</div>
|
||||
<div className="text-xs md:text-sm text-gray-400">Активные</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-3 md:p-4 rounded-lg text-center">
|
||||
<div className="text-lg md:text-2xl font-bold text-blue-500">{vpnConnections.length}</div>
|
||||
<div className="text-xs md:text-sm text-gray-400">VPN</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-3 md:p-4 rounded-lg text-center">
|
||||
<div className="text-lg md:text-2xl font-bold text-purple-500">
|
||||
{networkAdapters.filter(a => a.type === 'Wi-Fi').length}
|
||||
</div>
|
||||
<div className="text-xs md:text-sm text-gray-400">Wi-Fi</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
|
||||
{/* Сетевые адаптеры */}
|
||||
<div className="bg-gray-800 rounded-lg p-4 md:p-6">
|
||||
<h2 className="text-lg md:text-xl font-semibold text-white mb-3 md:mb-4">Сетевые интерфейсы</h2>
|
||||
{loading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto"></div>
|
||||
<p className="text-gray-400 mt-2">Загрузка данных...</p>
|
||||
</div>
|
||||
) : networkAdapters.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<div className="text-4xl mb-2">🔍</div>
|
||||
<p>Нет данных о сетевых интерфейсах</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
{networkAdapters.map((adapter, index) => (
|
||||
<div key={index} className="border border-gray-700 rounded-lg p-3 md:p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-xl">{getAdapterIcon(adapter.type)}</span>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white text-sm md:text-base">{adapter.name}</h3>
|
||||
<p className="text-gray-400 text-xs md:text-sm">{adapter.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-2 h-2 rounded-full ${getStatusColor(adapter.status)}`}></div>
|
||||
<span className="text-xs text-gray-400">{adapter.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs md:text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">IP:</span>
|
||||
<span className="text-white ml-1">{adapter.ipAddress}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">MAC:</span>
|
||||
<span className="text-white ml-1">{adapter.macAddress}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* VPN подключения */}
|
||||
<div className="bg-gray-800 rounded-lg p-4 md:p-6">
|
||||
<h2 className="text-lg md:text-xl font-semibold text-white mb-3 md:mb-4">VPN подключения</h2>
|
||||
{loading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto"></div>
|
||||
<p className="text-gray-400 mt-2">Загрузка данных...</p>
|
||||
</div>
|
||||
) : vpnConnections.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
<div className="text-4xl mb-2">🔒</div>
|
||||
<p>Нет активных VPN подключений</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
{vpnConnections.map((connection, index) => (
|
||||
<div key={index} className="border border-gray-700 rounded-lg p-3 md:p-4 bg-gradient-to-r from-blue-900/20 to-purple-900/20">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="font-semibold text-white text-sm md:text-base">{connection.name}</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-2 h-2 rounded-full ${getStatusColor(connection.status)}`}></div>
|
||||
<span className="text-xs text-gray-400">{connection.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-400 text-xs md:text-sm mb-2">{connection.description}</p>
|
||||
<div className="text-xs md:text-sm">
|
||||
<span className="text-gray-500">Интерфейс: </span>
|
||||
<span className="text-white">{connection.interface}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkMonitor;
|
||||
@@ -0,0 +1,199 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const NetworkTools = () => {
|
||||
const [activeTool, setActiveTool] = useState('traceroute');
|
||||
const [input, setInput] = useState('');
|
||||
const [results, setResults] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [portScanResults, setPortScanResults] = useState([]);
|
||||
|
||||
const tools = [
|
||||
{ id: 'traceroute', name: 'Traceroute', icon: '🔄', placeholder: 'example.com или IP' },
|
||||
{ id: 'whois', name: 'WHOIS Lookup', icon: '🔍', placeholder: 'domain.com' },
|
||||
{ id: 'port-scan', name: 'Port Scan', icon: '🔒', placeholder: '192.168.1.1' },
|
||||
{ id: 'dns-lookup', name: 'DNS Lookup', icon: '🌐', placeholder: 'domain.com' },
|
||||
{ id: 'speed-test', name: 'Speed Test', icon: '🚀', placeholder: 'Нажмите для запуска' }
|
||||
];
|
||||
|
||||
const executeTool = async () => {
|
||||
if (!input.trim() && activeTool !== 'speed-test') {
|
||||
alert('Введите значение для проверки');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setResults('Выполнение...');
|
||||
|
||||
try {
|
||||
let result;
|
||||
switch (activeTool) {
|
||||
case 'traceroute':
|
||||
result = await window.electronAPI.traceroute(input);
|
||||
break;
|
||||
case 'whois':
|
||||
result = await window.electronAPI.whois(input);
|
||||
break;
|
||||
case 'port-scan':
|
||||
result = await window.electronAPI.portScan(input, '21,22,80,443,8080,3306,5432,27017');
|
||||
if (result.success) {
|
||||
setPortScanResults(result.results);
|
||||
setResults(`Найдено открытых портов: ${result.results.filter(r => r.open).length}`);
|
||||
}
|
||||
break;
|
||||
case 'dns-lookup':
|
||||
result = await window.electronAPI.dnsLookup(input);
|
||||
break;
|
||||
case 'speed-test':
|
||||
result = await window.electronAPI.speedTest({});
|
||||
break;
|
||||
default:
|
||||
result = { success: false, error: 'Неизвестный инструмент' };
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
if (activeTool !== 'port-scan') {
|
||||
setResults(result.output || 'Успешно выполнено');
|
||||
}
|
||||
} else {
|
||||
setResults(`Ошибка: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
setResults(`Ошибка: ${error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getToolDescription = () => {
|
||||
const descriptions = {
|
||||
'traceroute': 'Трассировка маршрута до указанного хоста',
|
||||
'whois': 'Информация о домене из WHOIS базы данных',
|
||||
'port-scan': 'Сканирование открытых портов на указанном хосте',
|
||||
'dns-lookup': 'DNS запрос для получения IP адресов домена',
|
||||
'speed-test': 'Тест скорости интернет соединения'
|
||||
};
|
||||
return descriptions[activeTool] || '';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-white">Сетевые инструменты</h1>
|
||||
<p className="text-gray-400">Диагностика и анализ сетевых соединений</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* Боковая панель с инструментами */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-gray-800 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-white mb-4">Инструменты</h3>
|
||||
<div className="space-y-2">
|
||||
{tools.map(tool => (
|
||||
<button
|
||||
key={tool.id}
|
||||
onClick={() => {
|
||||
setActiveTool(tool.id);
|
||||
setResults('');
|
||||
setPortScanResults([]);
|
||||
if (tool.id === 'speed-test') setInput('');
|
||||
}}
|
||||
className={`w-full text-left p-3 rounded-lg transition-colors flex items-center gap-3 ${
|
||||
activeTool === tool.id
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg">{tool.icon}</span>
|
||||
<span>{tool.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Основная область */}
|
||||
<div className="lg:col-span-3">
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
{tools.find(t => t.id === activeTool)?.name}
|
||||
</h3>
|
||||
<p className="text-gray-400 text-sm">{getToolDescription()}</p>
|
||||
</div>
|
||||
|
||||
{/* Input area */}
|
||||
{activeTool !== 'speed-test' && (
|
||||
<div className="flex gap-3 mb-6">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder={tools.find(t => t.id === activeTool)?.placeholder}
|
||||
className="flex-1 bg-gray-700 border border-gray-600 rounded-lg px-4 py-2 text-white placeholder-gray-400 focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={executeTool}
|
||||
disabled={loading}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{loading ? '🔄' : '▶️'}
|
||||
{loading ? 'Выполнение...' : 'Запуск'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTool === 'speed-test' && (
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={executeTool}
|
||||
disabled={loading}
|
||||
className="w-full py-4 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 flex items-center justify-center gap-3 text-lg"
|
||||
>
|
||||
{loading ? '🔄' : '🚀'}
|
||||
{loading ? 'Тестирование скорости...' : 'Запустить Speed Test'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results area */}
|
||||
<div className="bg-gray-900 rounded-lg p-4 min-h-[200px]">
|
||||
{activeTool === 'port-scan' && portScanResults.length > 0 ? (
|
||||
<div>
|
||||
<h4 className="text-white font-semibold mb-3">Результаты сканирования портов:</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{portScanResults.map((result, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`p-3 rounded-lg border ${
|
||||
result.open
|
||||
? 'bg-green-900 border-green-600'
|
||||
: 'bg-gray-800 border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-white font-mono">Port {result.port}</span>
|
||||
<span className={`text-xs px-2 py-1 rounded ${
|
||||
result.open ? 'bg-green-600 text-white' : 'bg-gray-600 text-gray-300'
|
||||
}`}>
|
||||
{result.open ? 'OPEN' : 'CLOSED'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-gray-400 text-xs mt-1">{result.service}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<pre className="text-gray-300 whitespace-pre-wrap font-mono text-sm">
|
||||
{results}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkTools;
|
||||
@@ -0,0 +1,664 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
const Notes = () => {
|
||||
const [notes, setNotes] = useState([]);
|
||||
const [folders, setFolders] = useState([]);
|
||||
const [activeFolder, setActiveFolder] = useState('all');
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [showFolderForm, setShowFolderForm] = useState(false);
|
||||
const [editingNote, setEditingNote] = useState(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [viewMode, setViewMode] = useState('grid'); // 'grid' или 'list'
|
||||
const [newNote, setNewNote] = useState({
|
||||
title: '',
|
||||
content: '',
|
||||
folder: '',
|
||||
tags: [],
|
||||
color: 'blue',
|
||||
isRichText: false
|
||||
});
|
||||
const [newFolder, setNewFolder] = useState('');
|
||||
|
||||
const editorRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadNotes();
|
||||
loadFolders();
|
||||
}, []);
|
||||
|
||||
const loadNotes = async () => {
|
||||
if (window.electronAPI) {
|
||||
const result = await window.electronAPI.loadNotes();
|
||||
if (result.success) {
|
||||
setNotes(result.notes || []);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadFolders = async () => {
|
||||
if (window.electronAPI) {
|
||||
const result = await window.electronAPI.loadNoteFolders();
|
||||
if (result.success) {
|
||||
setFolders(result.folders || []);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const saveNotes = async (notesList) => {
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.saveNotes(notesList);
|
||||
}
|
||||
};
|
||||
|
||||
const saveFolders = async (foldersList) => {
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.saveNoteFolders(foldersList);
|
||||
}
|
||||
};
|
||||
|
||||
// Функции для форматирования текста
|
||||
const formatText = (command, value = null) => {
|
||||
document.execCommand(command, false, value);
|
||||
if (editorRef.current) {
|
||||
setNewNote({...newNote, content: editorRef.current.innerHTML});
|
||||
}
|
||||
};
|
||||
|
||||
const insertList = (type) => {
|
||||
formatText('insert' + (type === 'ordered' ? 'OrderedList' : 'UnorderedList'));
|
||||
};
|
||||
|
||||
const addNote = async () => {
|
||||
if (!newNote.title.trim()) {
|
||||
alert('Введите заголовок заметки');
|
||||
return;
|
||||
}
|
||||
|
||||
const note = {
|
||||
...newNote,
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const updatedNotes = [...notes, note];
|
||||
setNotes(updatedNotes);
|
||||
await saveNotes(updatedNotes);
|
||||
setShowAddForm(false);
|
||||
setNewNote({
|
||||
title: '',
|
||||
content: '',
|
||||
folder: '',
|
||||
tags: [],
|
||||
color: 'blue',
|
||||
isRichText: false
|
||||
});
|
||||
};
|
||||
|
||||
const updateNote = async () => {
|
||||
if (!editingNote.title.trim()) return;
|
||||
|
||||
const updatedNotes = notes.map(note =>
|
||||
note.id === editingNote.id
|
||||
? { ...editingNote, updatedAt: new Date().toISOString() }
|
||||
: note
|
||||
);
|
||||
|
||||
setNotes(updatedNotes);
|
||||
await saveNotes(updatedNotes);
|
||||
setEditingNote(null);
|
||||
};
|
||||
|
||||
const deleteNote = async (noteId) => {
|
||||
if (confirm('Удалить эту заметку?')) {
|
||||
const updatedNotes = notes.filter(note => note.id !== noteId);
|
||||
setNotes(updatedNotes);
|
||||
await saveNotes(updatedNotes);
|
||||
}
|
||||
};
|
||||
|
||||
const addFolder = async () => {
|
||||
if (!newFolder.trim()) {
|
||||
alert('Введите название папки');
|
||||
return;
|
||||
}
|
||||
|
||||
const folder = {
|
||||
id: Date.now().toString(),
|
||||
name: newFolder,
|
||||
color: 'gray'
|
||||
};
|
||||
|
||||
const updatedFolders = [...folders, folder];
|
||||
setFolders(updatedFolders);
|
||||
await saveFolders(updatedFolders);
|
||||
setShowFolderForm(false);
|
||||
setNewFolder('');
|
||||
};
|
||||
|
||||
const deleteFolder = async (folderId) => {
|
||||
if (confirm('Удалить папку? Заметки из этой папки будут перемещены в "Без папки"')) {
|
||||
const updatedNotes = notes.map(note =>
|
||||
note.folder === folderId ? { ...note, folder: '' } : note
|
||||
);
|
||||
setNotes(updatedNotes);
|
||||
await saveNotes(updatedNotes);
|
||||
|
||||
const updatedFolders = folders.filter(folder => folder.id !== folderId);
|
||||
setFolders(updatedFolders);
|
||||
await saveFolders(updatedFolders);
|
||||
}
|
||||
};
|
||||
|
||||
const getFolderNotesCount = (folderId) => {
|
||||
return notes.filter(note => note.folder === folderId).length;
|
||||
};
|
||||
|
||||
const filteredNotes = notes.filter(note => {
|
||||
const matchesFolder = activeFolder === 'all' || note.folder === activeFolder;
|
||||
const matchesSearch = note.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
note.content.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
note.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
return matchesFolder && matchesSearch;
|
||||
});
|
||||
|
||||
const getColorClass = (color) => {
|
||||
const colorMap = {
|
||||
blue: 'bg-blue-500',
|
||||
green: 'bg-green-500',
|
||||
red: 'bg-red-500',
|
||||
yellow: 'bg-yellow-500',
|
||||
purple: 'bg-purple-500',
|
||||
pink: 'bg-pink-500',
|
||||
gray: 'bg-gray-500'
|
||||
};
|
||||
return colorMap[color] || 'bg-blue-500';
|
||||
};
|
||||
|
||||
const formatContent = (content) => {
|
||||
if (!content) return '';
|
||||
|
||||
// Простая обработка переносов строк
|
||||
return content.split('\n').map((line, index) => (
|
||||
<div key={index}>
|
||||
{line || <br />}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 md:mb-6 gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl md:text-2xl font-bold text-white">Заметки</h1>
|
||||
<p className="text-gray-400 text-sm md:text-base">Организация мыслей и информации</p>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row gap-3 w-full md:w-auto">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setViewMode('grid')}
|
||||
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-primary-600 text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||
title="Сетка"
|
||||
>
|
||||
▦
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`p-2 rounded ${viewMode === 'list' ? 'bg-primary-600 text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||
title="Список"
|
||||
>
|
||||
≡
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative w-full md:w-64">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Поиск заметок..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-3 pr-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowFolderForm(true)}
|
||||
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors w-full md:w-auto text-sm"
|
||||
>
|
||||
+ Папка
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors w-full md:w-auto text-sm"
|
||||
>
|
||||
+ Заметка
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* Боковая панель с папками */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="bg-gray-800 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-white mb-4">Папки</h3>
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => setActiveFolder('all')}
|
||||
className={`w-full text-left p-3 rounded-lg transition-colors flex justify-between items-center ${
|
||||
activeFolder === 'all'
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span>Все заметки</span>
|
||||
<span className="text-xs bg-gray-600 px-2 py-1 rounded-full">
|
||||
{notes.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{folders.map(folder => (
|
||||
<div key={folder.id} className="flex justify-between items-center group">
|
||||
<button
|
||||
onClick={() => setActiveFolder(folder.id)}
|
||||
className={`flex-1 text-left p-3 rounded-lg transition-colors flex justify-between items-center ${
|
||||
activeFolder === folder.id
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'text-gray-300 hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span>{folder.name}</span>
|
||||
<span className="text-xs bg-gray-600 px-2 py-1 rounded-full">
|
||||
{getFolderNotesCount(folder.id)}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteFolder(folder.id)}
|
||||
className="text-gray-400 hover:text-red-500 transition-colors opacity-0 group-hover:opacity-100 ml-2"
|
||||
title="Удалить папку"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Быстрые шаблоны */}
|
||||
<div className="bg-gray-800 rounded-lg p-4 mt-4">
|
||||
<h3 className="font-semibold text-white mb-3">Шаблоны</h3>
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setNewNote({
|
||||
title: 'Команда для сервера',
|
||||
content: 'Команда: \nОписание: \nПримечания:',
|
||||
folder: '',
|
||||
tags: ['команда'],
|
||||
color: 'blue',
|
||||
isRichText: false
|
||||
});
|
||||
setShowAddForm(true);
|
||||
}}
|
||||
className="w-full text-left p-2 text-sm bg-gray-700 rounded hover:bg-gray-600 text-gray-300"
|
||||
>
|
||||
📝 Шаблон команды
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setNewNote({
|
||||
title: 'Конфигурация сервера',
|
||||
content: 'Сервер: \nIP: \nПорты: \nНастройки:',
|
||||
folder: '',
|
||||
tags: ['сервер'],
|
||||
color: 'green',
|
||||
isRichText: false
|
||||
});
|
||||
setShowAddForm(true);
|
||||
}}
|
||||
className="w-full text-left p-2 text-sm bg-gray-700 rounded hover:bg-gray-600 text-gray-300"
|
||||
>
|
||||
🖥️ Конфиг сервера
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Основная область с заметками */}
|
||||
<div className="lg:col-span-3">
|
||||
{/* Форма добавления/редактирования заметки */}
|
||||
{(showAddForm || editingNote) && (
|
||||
<div className="bg-gray-800 rounded-lg p-4 md:p-6 mb-6 border border-gray-700">
|
||||
<h3 className="text-lg font-semibold mb-4 text-white">
|
||||
{editingNote ? 'Редактировать заметку' : 'Новая заметка'}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Заголовок *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editingNote ? editingNote.title : newNote.title}
|
||||
onChange={(e) => editingNote
|
||||
? setEditingNote({...editingNote, title: e.target.value})
|
||||
: setNewNote({...newNote, title: e.target.value})
|
||||
}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
placeholder="Заголовок заметки"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Папка
|
||||
</label>
|
||||
<select
|
||||
value={editingNote ? editingNote.folder : newNote.folder}
|
||||
onChange={(e) => editingNote
|
||||
? setEditingNote({...editingNote, folder: e.target.value})
|
||||
: setNewNote({...newNote, folder: e.target.value})
|
||||
}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">Без папки</option>
|
||||
{folders.map(folder => (
|
||||
<option key={folder.id} value={folder.id}>{folder.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Цвет
|
||||
</label>
|
||||
<select
|
||||
value={editingNote ? editingNote.color : newNote.color}
|
||||
onChange={(e) => editingNote
|
||||
? setEditingNote({...editingNote, color: e.target.value})
|
||||
: setNewNote({...newNote, color: e.target.value})
|
||||
}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
>
|
||||
<option value="blue">Синий</option>
|
||||
<option value="green">Зеленый</option>
|
||||
<option value="red">Красный</option>
|
||||
<option value="yellow">Желтый</option>
|
||||
<option value="purple">Фиолетовый</option>
|
||||
<option value="pink">Розовый</option>
|
||||
<option value="gray">Серый</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Теги (через запятую)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editingNote ? editingNote.tags.join(', ') : newNote.tags.join(', ')}
|
||||
onChange={(e) => {
|
||||
const tags = e.target.value.split(',').map(tag => tag.trim()).filter(Boolean);
|
||||
editingNote
|
||||
? setEditingNote({...editingNote, tags})
|
||||
: setNewNote({...newNote, tags})
|
||||
}}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
placeholder="сервер, команда, настройка"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="block text-sm font-medium text-gray-300">
|
||||
Содержание
|
||||
</label>
|
||||
<div className="flex space-x-1">
|
||||
<button
|
||||
onClick={() => formatText('bold')}
|
||||
className="px-2 py-1 bg-gray-700 rounded text-sm text-white hover:bg-gray-600"
|
||||
title="Жирный"
|
||||
>
|
||||
<strong>B</strong>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => formatText('italic')}
|
||||
className="px-2 py-1 bg-gray-700 rounded text-sm text-white hover:bg-gray-600"
|
||||
title="Курсив"
|
||||
>
|
||||
<em>I</em>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => formatText('underline')}
|
||||
className="px-2 py-1 bg-gray-700 rounded text-sm text-white hover:bg-gray-600"
|
||||
title="Подчеркивание"
|
||||
>
|
||||
<u>U</u>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertList('unordered')}
|
||||
className="px-2 py-1 bg-gray-700 rounded text-sm text-white hover:bg-gray-600"
|
||||
title="Маркированный список"
|
||||
>
|
||||
• List
|
||||
</button>
|
||||
<button
|
||||
onClick={() => insertList('ordered')}
|
||||
className="px-2 py-1 bg-gray-700 rounded text-sm text-white hover:bg-gray-600"
|
||||
title="Нумерованный список"
|
||||
>
|
||||
1. List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={editorRef}
|
||||
contentEditable
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: editingNote ? editingNote.content : newNote.content
|
||||
}}
|
||||
onInput={(e) => {
|
||||
const content = e.target.innerHTML;
|
||||
editingNote
|
||||
? setEditingNote({...editingNote, content})
|
||||
: setNewNote({...newNote, content})
|
||||
}}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent min-h-[200px] overflow-y-auto"
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordWrap: 'break-word'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row justify-end space-y-2 md:space-y-0 md:space-x-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowAddForm(false);
|
||||
setEditingNote(null);
|
||||
}}
|
||||
className="px-4 py-2 border border-gray-600 text-gray-300 rounded-lg hover:bg-gray-700 transition-colors w-full md:w-auto"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
onClick={editingNote ? updateNote : addNote}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors w-full md:w-auto"
|
||||
>
|
||||
{editingNote ? 'Обновить' : 'Сохранить'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Форма добавления папки */}
|
||||
{showFolderForm && (
|
||||
<div className="bg-gray-800 rounded-lg p-4 md:p-6 mb-6 border border-gray-700">
|
||||
<h3 className="text-lg font-semibold mb-4 text-white">Новая папка</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Название папки *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newFolder}
|
||||
onChange={(e) => setNewFolder(e.target.value)}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
placeholder="Название папки"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row justify-end space-y-2 md:space-y-0 md:space-x-3">
|
||||
<button
|
||||
onClick={() => setShowFolderForm(false)}
|
||||
className="px-4 py-2 border border-gray-600 text-gray-300 rounded-lg hover:bg-gray-700 transition-colors w-full md:w-auto"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
onClick={addFolder}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors w-full md:w-auto"
|
||||
>
|
||||
Создать папку
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Список заметок */}
|
||||
{filteredNotes.length === 0 ? (
|
||||
<div className="text-center py-8 md:py-12">
|
||||
<div className="text-gray-400 text-4xl md:text-6xl mb-4">📝</div>
|
||||
<h3 className="text-lg font-medium text-white">Нет заметок</h3>
|
||||
<p className="text-gray-400 mt-1">
|
||||
{searchTerm ? 'Ничего не найдено' : 'Создайте вашу первую заметку'}
|
||||
</p>
|
||||
</div>
|
||||
) : viewMode === 'grid' ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6">
|
||||
{filteredNotes.map(note => (
|
||||
<div
|
||||
key={note.id}
|
||||
className="bg-gray-800 p-4 md:p-6 rounded-lg shadow-lg border border-gray-700 hover:border-primary-500 transition-colors"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-3 h-3 rounded-full ${getColorClass(note.color)}`}></div>
|
||||
<h3 className="font-semibold text-white text-base md:text-lg">{note.title}</h3>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => setEditingNote(note)}
|
||||
className="text-gray-400 hover:text-primary-500 transition-colors"
|
||||
title="Редактировать"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteNote(note.id)}
|
||||
className="text-gray-400 hover:text-red-500 transition-colors"
|
||||
title="Удалить"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<div
|
||||
className="text-gray-300 text-sm line-clamp-4 rich-text-content"
|
||||
dangerouslySetInnerHTML={{ __html: note.content }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{note.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{note.tags.map(tag => (
|
||||
<span key={tag} className="px-2 py-1 bg-gray-700 text-gray-300 rounded text-xs">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center pt-3 border-t border-gray-700">
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(note.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
{note.folder && (
|
||||
<span className="text-xs px-2 py-1 bg-primary-500 text-white rounded">
|
||||
{folders.find(f => f.id === note.folder)?.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredNotes.map(note => (
|
||||
<div
|
||||
key={note.id}
|
||||
className="bg-gray-800 p-4 md:p-6 rounded-lg shadow-lg border border-gray-700 hover:border-primary-500 transition-colors"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex items-center space-x-3 flex-1">
|
||||
<div className={`w-3 h-3 rounded-full ${getColorClass(note.color)}`}></div>
|
||||
<h3 className="font-semibold text-white text-base md:text-lg flex-1">{note.title}</h3>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => setEditingNote(note)}
|
||||
className="text-gray-400 hover:text-primary-500 transition-colors"
|
||||
title="Редактировать"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteNote(note.id)}
|
||||
className="text-gray-400 hover:text-red-500 transition-colors"
|
||||
title="Удалить"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<div
|
||||
className="text-gray-300 text-sm rich-text-content"
|
||||
dangerouslySetInnerHTML={{ __html: note.content }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-3 border-t border-gray-700">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{note.tags.map(tag => (
|
||||
<span key={tag} className="px-2 py-1 bg-gray-700 text-gray-300 rounded text-xs">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(note.updatedAt).toLocaleDateString()}
|
||||
</span>
|
||||
{note.folder && (
|
||||
<span className="text-xs px-2 py-1 bg-primary-500 text-white rounded">
|
||||
{folders.find(f => f.id === note.folder)?.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notes;
|
||||
@@ -0,0 +1,300 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const PasswordManager = () => {
|
||||
const [passwords, setPasswords] = useState([]);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [newPassword, setNewPassword] = useState({
|
||||
title: '',
|
||||
server: '',
|
||||
username: '',
|
||||
password: '',
|
||||
category: 'server',
|
||||
notes: ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadPasswords();
|
||||
}, []);
|
||||
|
||||
const loadPasswords = async () => {
|
||||
if (window.electronAPI) {
|
||||
const result = await window.electronAPI.loadPasswords();
|
||||
if (result.success) {
|
||||
setPasswords(result.passwords || []);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const savePasswords = async (passwordsList) => {
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.savePasswords(passwordsList);
|
||||
}
|
||||
};
|
||||
|
||||
const addPassword = async () => {
|
||||
if (!newPassword.title) {
|
||||
alert('Заполните название');
|
||||
return;
|
||||
}
|
||||
|
||||
const password = {
|
||||
...newPassword,
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const updatedPasswords = [...passwords, password];
|
||||
setPasswords(updatedPasswords);
|
||||
await savePasswords(updatedPasswords);
|
||||
setShowAddForm(false);
|
||||
setNewPassword({
|
||||
title: '',
|
||||
server: '',
|
||||
username: '',
|
||||
password: '',
|
||||
category: 'server',
|
||||
notes: ''
|
||||
});
|
||||
};
|
||||
|
||||
const deletePassword = async (passwordId) => {
|
||||
if (confirm('Удалить эту запись?')) {
|
||||
const updatedPasswords = passwords.filter(p => p.id !== passwordId);
|
||||
setPasswords(updatedPasswords);
|
||||
await savePasswords(updatedPasswords);
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text) => {
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.copyToClipboard(text);
|
||||
alert('Скопировано в буфер обмена');
|
||||
}
|
||||
};
|
||||
|
||||
const generatePassword = () => {
|
||||
const length = 16;
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
|
||||
let password = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||
}
|
||||
setNewPassword({...newPassword, password});
|
||||
};
|
||||
|
||||
const filteredPasswords = passwords.filter(password =>
|
||||
password.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
password.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
password.category.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 md:mb-6 gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl md:text-2xl font-bold text-white">Менеджер паролей</h1>
|
||||
<p className="text-gray-400 text-sm md:text-base">Безопасное хранение учетных данных</p>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row gap-3 w-full md:w-auto">
|
||||
<div className="relative w-full md:w-64">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Поиск паролей..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-3 pr-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors w-full md:w-auto text-sm"
|
||||
>
|
||||
+ Добавить запись
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showAddForm && (
|
||||
<div className="bg-gray-800 p-4 md:p-6 rounded-lg shadow-lg mb-4 md:mb-6 border border-gray-700">
|
||||
<h3 className="text-lg font-semibold mb-4 text-white">Добавить новую запись</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4">
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Название *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newPassword.title}
|
||||
onChange={(e) => setNewPassword({...newPassword, title: e.target.value})}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||||
placeholder="Root доступ к серверу"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Сервер
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newPassword.server}
|
||||
onChange={(e) => setNewPassword({...newPassword, server: e.target.value})}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||||
placeholder="server.example.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Категория
|
||||
</label>
|
||||
<select
|
||||
value={newPassword.category}
|
||||
onChange={(e) => setNewPassword({...newPassword, category: e.target.value})}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||||
>
|
||||
<option value="server">Сервер</option>
|
||||
<option value="database">База данных</option>
|
||||
<option value="web">Веб-сайт</option>
|
||||
<option value="other">Другое</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Имя пользователя
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newPassword.username}
|
||||
onChange={(e) => setNewPassword({...newPassword, username: e.target.value})}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||||
placeholder="admin"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Пароль
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newPassword.password}
|
||||
onChange={(e) => setNewPassword({...newPassword, password: e.target.value})}
|
||||
className="flex-1 bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||||
placeholder="Пароль"
|
||||
/>
|
||||
<button
|
||||
onClick={generatePassword}
|
||||
className="px-3 bg-primary-600 text-white rounded-lg hover:bg-primary-500 text-sm"
|
||||
>
|
||||
🎲
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Заметки
|
||||
</label>
|
||||
<textarea
|
||||
value={newPassword.notes}
|
||||
onChange={(e) => setNewPassword({...newPassword, notes: e.target.value})}
|
||||
rows="3"
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg p-3 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent text-sm"
|
||||
placeholder="Дополнительная информация..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row justify-end space-y-2 md:space-y-0 md:space-x-3 mt-4">
|
||||
<button
|
||||
onClick={() => setShowAddForm(false)}
|
||||
className="px-4 py-2 border border-gray-600 text-gray-300 rounded-lg hover:bg-gray-700 transition-colors w-full md:w-auto text-sm"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
onClick={addPassword}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors w-full md:w-auto text-sm"
|
||||
>
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredPasswords.length === 0 ? (
|
||||
<div className="text-center py-8 md:py-12">
|
||||
<div className="text-gray-400 text-4xl md:text-6xl mb-4">🔐</div>
|
||||
<h3 className="text-lg font-medium text-white">Нет сохраненных паролей</h3>
|
||||
<p className="text-gray-400 mt-1">
|
||||
{searchTerm ? 'Ничего не найдено' : 'Добавьте вашу первую запись'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
|
||||
{filteredPasswords.map(password => (
|
||||
<div key={password.id} className="bg-gray-800 p-4 md:p-6 rounded-lg shadow-lg border border-gray-700">
|
||||
<div className="flex justify-between items-start mb-3 md:mb-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-white text-base md:text-lg">{password.title}</h3>
|
||||
{password.server && (
|
||||
<p className="text-gray-400 text-sm mt-1">{password.server}</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => deletePassword(password.id)}
|
||||
className="text-gray-400 hover:text-red-500 transition-colors"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-400">Пользователь:</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-white font-mono">{password.username}</span>
|
||||
<button
|
||||
onClick={() => copyToClipboard(password.username)}
|
||||
className="text-gray-400 hover:text-primary-500 transition-colors"
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-400">Пароль:</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-white font-mono">••••••••</span>
|
||||
<button
|
||||
onClick={() => copyToClipboard(password.password)}
|
||||
className="text-gray-400 hover:text-primary-500 transition-colors"
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{password.notes && (
|
||||
<div className="pt-3 border-t border-gray-700">
|
||||
<p className="text-sm text-gray-400 italic">{password.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center pt-3 border-t border-gray-700">
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(password.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
<span className="text-xs px-2 py-1 bg-primary-500 text-white rounded">
|
||||
{password.category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordManager;
|
||||
@@ -0,0 +1,210 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const ServerHealth = () => {
|
||||
const [servers, setServers] = useState([]);
|
||||
const [healthData, setHealthData] = useState({});
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadServers();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let interval;
|
||||
if (autoRefresh) {
|
||||
interval = setInterval(() => {
|
||||
refreshAllHealth();
|
||||
}, 30000);
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [autoRefresh, servers]);
|
||||
|
||||
const loadServers = async () => {
|
||||
if (window.electronAPI) {
|
||||
const result = await window.electronAPI.loadServers();
|
||||
if (result.success) {
|
||||
setServers(result.servers);
|
||||
refreshAllHealth();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refreshAllHealth = async () => {
|
||||
setLoading(true);
|
||||
const newHealthData = {};
|
||||
|
||||
for (const server of servers) {
|
||||
if (window.electronAPI) {
|
||||
const result = await window.electronAPI.getServerHealth(server);
|
||||
if (result.success) {
|
||||
newHealthData[server.id] = result.health;
|
||||
} else {
|
||||
newHealthData[server.id] = {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
disk: 0,
|
||||
uptime: 'N/A',
|
||||
load: 'N/A',
|
||||
online: false,
|
||||
error: result.error
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setHealthData(newHealthData);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const getStatusColor = (value, thresholds = [70, 90]) => {
|
||||
if (value < thresholds[0]) return 'bg-green-500';
|
||||
if (value < thresholds[1]) return 'bg-yellow-500';
|
||||
return 'bg-red-500';
|
||||
};
|
||||
|
||||
const getStatusText = (value, thresholds = [70, 90]) => {
|
||||
if (value < thresholds[0]) return 'Норма';
|
||||
if (value < thresholds[1]) return 'Нагрузка';
|
||||
return 'Критично';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Health Dashboard</h1>
|
||||
<p className="text-gray-400">Мониторинг состояния серверов в реальном времени</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoRefresh}
|
||||
onChange={(e) => setAutoRefresh(e.target.checked)}
|
||||
className="rounded border-gray-600 bg-gray-700"
|
||||
/>
|
||||
<span className="text-gray-300 text-sm">Автообновление (30с)</span>
|
||||
</label>
|
||||
<button
|
||||
onClick={refreshAllHealth}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{loading ? '🔄' : '📊'}
|
||||
{loading ? 'Обновление...' : 'Обновить'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{servers.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 text-6xl mb-4">🏥</div>
|
||||
<h3 className="text-lg font-medium text-white">Нет серверов для мониторинга</h3>
|
||||
<p className="text-gray-400 mt-1">Добавьте серверы в разделе "Серверы"</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{servers.map(server => {
|
||||
const health = healthData[server.id] || {};
|
||||
|
||||
return (
|
||||
<div key={server.id} className="bg-gray-800 rounded-lg p-6 border border-gray-700 hover:border-blue-500 transition-colors">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg text-white">{server.name}</h3>
|
||||
<p className="text-gray-400 text-sm">{server.ip}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full ${health.online ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||
<span className={`text-sm font-medium ${health.online ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{health.online ? 'Online' : 'Offline'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{health.online ? (
|
||||
<div className="space-y-4">
|
||||
{/* CPU */}
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-gray-400">Процессор</span>
|
||||
<span className="text-white font-medium">
|
||||
{health.cpu?.toFixed(1)}% - {getStatusText(health.cpu)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${getStatusColor(health.cpu)}`}
|
||||
style={{ width: `${Math.min(100, health.cpu)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Memory */}
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-gray-400">Память</span>
|
||||
<span className="text-white font-medium">
|
||||
{health.memory?.toFixed(1)}% - {getStatusText(health.memory)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${getStatusColor(health.memory)}`}
|
||||
style={{ width: `${Math.min(100, health.memory)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Disk */}
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-gray-400">Диск</span>
|
||||
<span className="text-white font-medium">
|
||||
{health.disk?.toFixed(1)}% - {getStatusText(health.disk)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${getStatusColor(health.disk)}`}
|
||||
style={{ width: `${Math.min(100, health.disk)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Info */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm pt-2 border-t border-gray-700">
|
||||
<div>
|
||||
<span className="text-gray-400">Uptime:</span>
|
||||
<div className="text-white font-medium">{health.uptime}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">Load:</span>
|
||||
<div className="text-white font-medium">{health.load}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4 text-gray-400">
|
||||
<div className="text-2xl mb-2">❌</div>
|
||||
<p>Сервер недоступен</p>
|
||||
{health.error && (
|
||||
<p className="text-xs mt-1 text-red-400">{health.error}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-gray-500 mt-4 text-center">
|
||||
Обновлено: {health.timestamp ? new Date(health.timestamp).toLocaleTimeString() : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerHealth;
|
||||
@@ -0,0 +1,384 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const ServerList = () => {
|
||||
const [servers, setServers] = useState([]);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [filter, setFilter] = useState('all');
|
||||
const [newServer, setNewServer] = useState({
|
||||
name: '',
|
||||
ip: '',
|
||||
port: '22',
|
||||
username: 'root',
|
||||
password: '',
|
||||
webPort: '51821',
|
||||
sshKey: '',
|
||||
tags: [],
|
||||
status: 'unknown'
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadServers();
|
||||
}, []);
|
||||
|
||||
const loadServers = async () => {
|
||||
if (window.electronAPI) {
|
||||
const result = await window.electronAPI.loadServers();
|
||||
if (result.success) {
|
||||
// Добавляем статус для каждого сервера
|
||||
const serversWithStatus = result.servers.map(server => ({
|
||||
...server,
|
||||
status: server.status || 'unknown'
|
||||
}));
|
||||
setServers(serversWithStatus);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const saveServers = async (serversList) => {
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.saveServers(serversList);
|
||||
}
|
||||
};
|
||||
|
||||
const addServer = async () => {
|
||||
if (!newServer.name || !newServer.ip) {
|
||||
alert('Заполните название и IP адрес');
|
||||
return;
|
||||
}
|
||||
|
||||
const server = {
|
||||
...newServer,
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const updatedServers = [...servers, server];
|
||||
setServers(updatedServers);
|
||||
await saveServers(updatedServers);
|
||||
setShowAddForm(false);
|
||||
setNewServer({
|
||||
name: '',
|
||||
ip: '',
|
||||
port: '22',
|
||||
username: 'root',
|
||||
password: '',
|
||||
webPort: '51821',
|
||||
sshKey: '',
|
||||
tags: [],
|
||||
status: 'unknown'
|
||||
});
|
||||
};
|
||||
|
||||
const deleteServer = async (serverId) => {
|
||||
if (confirm('Удалить сервер?')) {
|
||||
const updatedServers = servers.filter(s => s.id !== serverId);
|
||||
setServers(updatedServers);
|
||||
await saveServers(updatedServers);
|
||||
}
|
||||
};
|
||||
|
||||
const connectSSH = async (server) => {
|
||||
if (window.electronAPI) {
|
||||
// Логируем действие
|
||||
await window.electronAPI.logAction(`SSH подключение к серверу ${server.name} (${server.ip})`);
|
||||
await window.electronAPI.connectSSH(server);
|
||||
}
|
||||
};
|
||||
|
||||
const openWebInterface = async (server) => {
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.logAction(`Открытие веб-интерфейса сервера ${server.name}`);
|
||||
await window.electronAPI.openWebInterface(server);
|
||||
}
|
||||
};
|
||||
|
||||
const checkServerStatus = async (server) => {
|
||||
// Простая проверка доступности
|
||||
const updatedServers = servers.map(s =>
|
||||
s.id === server.id ? { ...s, status: 'checking' } : s
|
||||
);
|
||||
setServers(updatedServers);
|
||||
|
||||
// Имитация проверки статуса
|
||||
setTimeout(() => {
|
||||
const randomStatus = Math.random() > 0.5 ? 'online' : 'offline';
|
||||
const finalServers = servers.map(s =>
|
||||
s.id === server.id ? { ...s, status: randomStatus } : s
|
||||
);
|
||||
setServers(finalServers);
|
||||
saveServers(finalServers);
|
||||
|
||||
// Уведомление если сервер недоступен
|
||||
if (randomStatus === 'offline' && window.electronAPI) {
|
||||
window.electronAPI.showNotification(
|
||||
'Сервер недоступен',
|
||||
`Сервер ${server.name} (${server.ip}) не отвечает`
|
||||
);
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'online': return 'bg-green-500';
|
||||
case 'offline': return 'bg-red-500';
|
||||
case 'checking': return 'bg-yellow-500';
|
||||
default: return 'bg-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'online': return 'Доступен';
|
||||
case 'offline': return 'Недоступен';
|
||||
case 'checking': return 'Проверка...';
|
||||
default: return 'Неизвестно';
|
||||
}
|
||||
};
|
||||
|
||||
const filteredServers = filter === 'all'
|
||||
? servers
|
||||
: servers.filter(server => server.tags.includes(filter));
|
||||
|
||||
const allTags = [...new Set(servers.flatMap(server => server.tags))];
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 md:mb-6 gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl md:text-2xl font-bold text-white">Управление серверами</h1>
|
||||
<p className="text-gray-400 text-sm md:text-base">Всего серверов: {servers.length}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors w-full md:w-auto"
|
||||
>
|
||||
+ Добавить сервер
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Фильтр по тегам */}
|
||||
{allTags.length > 0 && (
|
||||
<div className="mb-4 md:mb-6">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => setFilter('all')}
|
||||
className={`px-3 py-1 rounded-full text-sm ${
|
||||
filter === 'all'
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
Все
|
||||
</button>
|
||||
{allTags.map(tag => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => setFilter(tag)}
|
||||
className={`px-3 py-1 rounded-full text-sm ${
|
||||
filter === tag
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAddForm && (
|
||||
<div className="bg-gray-800 p-4 md:p-6 rounded-lg shadow-lg mb-4 md:mb-6 border border-gray-700">
|
||||
<h3 className="text-lg font-semibold mb-4 text-white">Добавить сервер</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4">
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Название *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newServer.name}
|
||||
onChange={(e) => setNewServer({...newServer, name: e.target.value})}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
placeholder="Мой сервер"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
IP адрес *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newServer.ip}
|
||||
onChange={(e) => setNewServer({...newServer, ip: e.target.value})}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
placeholder="192.168.1.100"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Теги (через запятую)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newServer.tags.join(', ')}
|
||||
onChange={(e) => setNewServer({...newServer, tags: e.target.value.split(',').map(tag => tag.trim()).filter(Boolean)})}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
placeholder="production, web, database"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
SSH порт
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={newServer.port}
|
||||
onChange={(e) => setNewServer({...newServer, port: e.target.value})}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
placeholder="22"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Веб-порт
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={newServer.webPort}
|
||||
onChange={(e) => setNewServer({...newServer, webPort: e.target.value})}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
placeholder="51821"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Имя пользователя
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newServer.username}
|
||||
onChange={(e) => setNewServer({...newServer, username: e.target.value})}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
placeholder="root"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Пароль
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newServer.password}
|
||||
onChange={(e) => setNewServer({...newServer, password: e.target.value})}
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
placeholder="Пароль"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row justify-end space-y-2 md:space-y-0 md:space-x-3 mt-4">
|
||||
<button
|
||||
onClick={() => setShowAddForm(false)}
|
||||
className="px-4 py-2 border border-gray-600 text-gray-300 rounded-lg hover:bg-gray-700 transition-colors w-full md:w-auto"
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
onClick={addServer}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors w-full md:w-auto"
|
||||
>
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{servers.length === 0 ? (
|
||||
<div className="text-center py-8 md:py-12">
|
||||
<div className="text-gray-400 text-4xl md:text-6xl mb-4">🖥️</div>
|
||||
<h3 className="text-lg font-medium text-white">Нет серверов</h3>
|
||||
<p className="text-gray-400 mt-1">Добавьте ваш первый сервер</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
|
||||
{filteredServers.map(server => (
|
||||
<div key={server.id} className="bg-gray-800 p-4 md:p-6 rounded-lg shadow-lg border border-gray-700">
|
||||
<div className="flex justify-between items-start mb-3 md:mb-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-white text-base md:text-lg">{server.name}</h3>
|
||||
<p className="text-gray-400 text-sm">{server.ip}</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-2 h-2 rounded-full ${getStatusColor(server.status)}`}></div>
|
||||
<span className="text-xs text-gray-400">{getStatusText(server.status)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Теги сервера */}
|
||||
{server.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-3 md:mb-4">
|
||||
{server.tags.map(tag => (
|
||||
<span key={tag} className="px-2 py-1 bg-primary-500 text-white rounded text-xs">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 text-sm text-gray-300 mb-4">
|
||||
<div className="flex justify-between">
|
||||
<span>SSH порт:</span>
|
||||
<span className="text-white">{server.port}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Веб-порт:</span>
|
||||
<span className="text-white">{server.webPort}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Пользователь:</span>
|
||||
<span className="text-white">{server.username}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => checkServerStatus(server)}
|
||||
className="flex-1 px-2 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 transition-colors text-sm"
|
||||
>
|
||||
Проверить
|
||||
</button>
|
||||
<button
|
||||
onClick={() => connectSSH(server)}
|
||||
className="flex-1 px-2 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm"
|
||||
>
|
||||
SSH
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openWebInterface(server)}
|
||||
className="flex-1 px-2 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors text-sm"
|
||||
>
|
||||
Веб
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mt-3 pt-3 border-t border-gray-700">
|
||||
<button
|
||||
onClick={() => deleteServer(server.id)}
|
||||
className="text-gray-400 hover:text-red-500 transition-colors text-sm"
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(server.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerList;
|
||||
@@ -0,0 +1,207 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const ServerMonitoring = () => {
|
||||
const [servers, setServers] = useState([]);
|
||||
const [monitoringData, setMonitoringData] = useState({});
|
||||
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadServers();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoRefresh) {
|
||||
const interval = setInterval(() => {
|
||||
servers.forEach(server => {
|
||||
checkServerStatus(server);
|
||||
});
|
||||
}, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [autoRefresh, servers]);
|
||||
|
||||
const loadServers = async () => {
|
||||
if (window.electronAPI) {
|
||||
const result = await window.electronAPI.loadServers();
|
||||
if (result.success) {
|
||||
setServers(result.servers);
|
||||
result.servers.forEach(server => checkServerStatus(server));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkServerStatus = async (server) => {
|
||||
if (!window.electronAPI) return;
|
||||
|
||||
// Ping проверка
|
||||
const pingResult = await window.electronAPI.pingServer(server);
|
||||
|
||||
// SNMP мониторинг (если доступен)
|
||||
let snmpData = null;
|
||||
try {
|
||||
const snmpResult = await window.electronAPI.getSnmpData(server, 'public');
|
||||
if (snmpResult.success) snmpData = snmpResult.data;
|
||||
} catch (error) {}
|
||||
|
||||
// SSH мониторинг (если есть учетные данные)
|
||||
let sshStats = null;
|
||||
if (server.username && server.password) {
|
||||
try {
|
||||
const sshResult = await window.electronAPI.getSshStats(server);
|
||||
if (sshResult.success) sshStats = sshResult.stats;
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
setMonitoringData(prev => ({
|
||||
...prev,
|
||||
[server.id]: {
|
||||
ping: pingResult,
|
||||
snmp: snmpData,
|
||||
ssh: sshStats,
|
||||
lastUpdate: new Date()
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const getStatusColor = (online) => {
|
||||
return online ? 'bg-green-500' : 'bg-red-500';
|
||||
};
|
||||
|
||||
const getUsageColor = (percent) => {
|
||||
if (percent < 70) return 'bg-green-500';
|
||||
if (percent < 90) return 'bg-yellow-500';
|
||||
return 'bg-red-500';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-800 dark:text-white">Мониторинг серверов</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">Реальное время состояние серверов</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoRefresh}
|
||||
onChange={(e) => setAutoRefresh(e.target.checked)}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Автообновление (30с)</span>
|
||||
</label>
|
||||
<button
|
||||
onClick={loadServers}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
|
||||
>
|
||||
Обновить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{servers.map(server => {
|
||||
const data = monitoringData[server.id];
|
||||
const isOnline = data?.ping?.online;
|
||||
|
||||
return (
|
||||
<div key={server.id} className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg text-gray-800 dark:text-white">{server.name}</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{server.ip}</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-3 h-3 rounded-full ${getStatusColor(isOnline)}`}></div>
|
||||
<span className={`text-sm font-medium ${isOnline ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{isOnline ? 'Онлайн' : 'Офлайн'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data && (
|
||||
<div className="space-y-4">
|
||||
{/* Ping информация */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-600 dark:text-gray-400">Ping:</span>
|
||||
<div className="font-semibold text-gray-800 dark:text-white">
|
||||
{data.ping?.responseTime ? `${data.ping.responseTime}ms` : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600 dark:text-gray-400">Потери:</span>
|
||||
<div className="font-semibold text-gray-800 dark:text-white">
|
||||
{data.ping?.packetLoss ? `${data.ping.packetLoss}%` : 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CPU Usage */}
|
||||
{(data.snmp?.cpuLoad || data.ssh?.cpuLoad) && (
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-gray-600 dark:text-gray-400">Загрузка CPU</span>
|
||||
<span className="font-semibold text-gray-800 dark:text-white">
|
||||
{Math.round(data.ssh?.cpuLoad || data.snmp?.cpuLoad)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${getUsageColor(data.ssh?.cpuLoad || data.snmp?.cpuLoad)}`}
|
||||
style={{ width: `${Math.min(100, data.ssh?.cpuLoad || data.snmp?.cpuLoad)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Memory Usage */}
|
||||
{(data.ssh?.memoryUsed && data.ssh?.memoryTotal) && (
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-gray-600 dark:text-gray-400">Память</span>
|
||||
<span className="font-semibold text-gray-800 dark:text-white">
|
||||
{Math.round((data.ssh.memoryUsed / data.ssh.memoryTotal) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${getUsageColor((data.ssh.memoryUsed / data.ssh.memoryTotal) * 100)}`}
|
||||
style={{ width: `${Math.min(100, (data.ssh.memoryUsed / data.ssh.memoryTotal) * 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{data.ssh.memoryUsed}MB / {data.ssh.memoryTotal}MB
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Last update */}
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 border-t pt-2">
|
||||
Обновлено: {data.lastUpdate?.toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!data && (
|
||||
<div className="text-center py-4 text-gray-500 dark:text-gray-400">
|
||||
Загрузка данных...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{servers.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 text-6xl mb-4">📊</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Нет серверов для мониторинга</h3>
|
||||
<p className="text-gray-500 dark:text-gray-400 mt-1">Добавьте серверы в разделе "Серверы"</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerMonitoring;
|
||||
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
|
||||
const Settings = () => {
|
||||
const handleExport = async () => {
|
||||
if (window.electronAPI) {
|
||||
const servers = await window.electronAPI.loadServers();
|
||||
const passwords = await window.electronAPI.loadPasswords();
|
||||
const templates = await window.electronAPI.loadCommandTemplates();
|
||||
|
||||
const data = {
|
||||
servers: servers.success ? servers.servers : [],
|
||||
passwords: passwords.success ? passwords.passwords : [],
|
||||
templates: templates.success ? templates.templates : [],
|
||||
exportDate: new Date().toISOString()
|
||||
};
|
||||
|
||||
const result = await window.electronAPI.exportData(data);
|
||||
if (result.success) {
|
||||
alert('Данные успешно экспортированы!');
|
||||
} else {
|
||||
alert(`Ошибка экспорта: ${result.error}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (window.electronAPI) {
|
||||
const result = await window.electronAPI.importData();
|
||||
if (result.success) {
|
||||
const data = result.data;
|
||||
|
||||
if (data.servers) {
|
||||
await window.electronAPI.saveServers(data.servers);
|
||||
}
|
||||
if (data.passwords) {
|
||||
await window.electronAPI.savePasswords(data.passwords);
|
||||
}
|
||||
if (data.templates) {
|
||||
await window.electronAPI.saveCommandTemplates(data.templates);
|
||||
}
|
||||
|
||||
alert('Данные успешно импортированы! Перезагрузите приложение.');
|
||||
} else {
|
||||
alert(`Ошибка импорта: ${result.error}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-white">Настройки</h1>
|
||||
<p className="text-gray-400">Управление данными приложения</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Экспорт данных</h3>
|
||||
<p className="text-gray-400 mb-4">Сохраните все данные в файл для резервного копирования</p>
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="w-full bg-primary-600 text-white py-2 px-4 rounded-lg hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
Экспортировать данные
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Импорт данных</h3>
|
||||
<p className="text-gray-400 mb-4">Восстановите данные из ранее созданного файла</p>
|
||||
<button
|
||||
onClick={handleImport}
|
||||
className="w-full bg-primary-600 text-white py-2 px-4 rounded-lg hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
Импортировать данные
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 bg-gray-800 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Информация о приложении</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-400">Версия:</span>
|
||||
<span className="text-white ml-2">2.0</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">Платформа:</span>
|
||||
<span className="text-white ml-2">{navigator.platform}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">Electron:</span>
|
||||
<span className="text-white ml-2">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
Reference in New Issue
Block a user