feat: initial commit - ServerManager Pro v2.0.0

This commit is contained in:
2025-11-26 23:21:44 +03:00
commit af51c68d7f
39 changed files with 23191 additions and 0 deletions
+247
View File
@@ -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;
+369
View File
@@ -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;
+275
View File
@@ -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;
+206
View File
@@ -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;
+199
View File
@@ -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;
+664
View File
@@ -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;
+300
View File
@@ -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;
+210
View File
@@ -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;
+384
View File
@@ -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;
+207
View File
@@ -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;
+101
View File
@@ -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;