feat: initial commit - ServerManager Pro v2.0.0
This commit is contained in:
+42
@@ -0,0 +1,42 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# Production builds
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -0,0 +1,103 @@
|
||||
# Инструкции по сборке
|
||||
|
||||
## 🏗️ Система сборки
|
||||
|
||||
### Основные скрипты:
|
||||
- **`build.sh`** - основной интерфейс для разработки
|
||||
- **`build.prod.sh`** - продакшен сборка для Linux и Windows
|
||||
|
||||
### Команды сборки:
|
||||
|
||||
```bash
|
||||
# Разработка
|
||||
./build.sh dev # Запуск в режиме разработки
|
||||
|
||||
# Сборка
|
||||
./build.sh build # Сборка для всех платформ
|
||||
./build.sh build-linux # Только Linux (.deb)
|
||||
./build.sh build-windows # Только Windows (.exe)
|
||||
|
||||
# Утилиты
|
||||
./build.sh clean # Очистка сборок
|
||||
./build.sh install-linux # Установка на Linux
|
||||
./build.sh status # Статус сборки
|
||||
./build.sh version # Показать версию
|
||||
```
|
||||
|
||||
## 🔄 Процесс сборки
|
||||
|
||||
### 1. Подготовка:
|
||||
```bash
|
||||
# Убедитесь что Node.js установлен
|
||||
node --version # Должно быть 16+
|
||||
|
||||
# Установите зависимости
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Разработка:
|
||||
```bash
|
||||
./build.sh dev
|
||||
```
|
||||
Запускает React dev server и Electron с горячей перезагрузкой.
|
||||
|
||||
### 3. Продакшен сборка:
|
||||
```bash
|
||||
./build.sh build
|
||||
```
|
||||
Создает:
|
||||
- `dist/servermanager-pro_*.deb` (Linux)
|
||||
- `dist/servermanager-pro_*.exe` (Windows)
|
||||
|
||||
## 📦 Дистрибуция
|
||||
|
||||
### Создание релиза:
|
||||
|
||||
1. Обновите версию:
|
||||
```bash
|
||||
npm version patch # 2.0.0 → 2.0.1
|
||||
# или
|
||||
npm version minor # 2.0.0 → 2.1.0
|
||||
```
|
||||
|
||||
2. Соберите пакеты:
|
||||
```bash
|
||||
./build.sh build
|
||||
```
|
||||
|
||||
3. Протестируйте пакеты
|
||||
|
||||
4. Создайте релиз на GitHub с файлами из `dist/`
|
||||
|
||||
## 🐛 Устранение неисправностей
|
||||
|
||||
### Проблемы со сборкой:
|
||||
```bash
|
||||
# Очистите и пересоберите
|
||||
./build.sh clean
|
||||
npm install
|
||||
./build.sh build
|
||||
```
|
||||
|
||||
### Проблемы с зависимостями:
|
||||
```bash
|
||||
# Переустановите зависимости
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
```
|
||||
|
||||
### Проблемы с Electron:
|
||||
```bash
|
||||
# Пересоберите Electron зависимости
|
||||
npm rebuild
|
||||
```
|
||||
|
||||
## 🔧 Кастомизация
|
||||
|
||||
### Изменение иконки:
|
||||
1. Положите `icon.png` в `public/`
|
||||
2. Пересоберите проект
|
||||
|
||||
### Изменение названия:
|
||||
Отредактируйте `productName` в `package.json`
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# ServerManager Pro - Руководство разработчика
|
||||
|
||||
## 🏗️ Структура проекта
|
||||
wireguard-manager/
|
||||
├── 📁 installers/ # Скрипты установки
|
||||
├── 📁 public/ # Статические файлы Electron
|
||||
├── 📁 scripts/ # Вспомогательные скрипты
|
||||
├── 📁 src/ # Исходный код React
|
||||
│ ├── 📁 components/ # Компоненты приложения
|
||||
│ └── 📄 App.js # Главный компонент
|
||||
├── 📄 build.sh # Основной скрипт сборки
|
||||
├── 📄 build.prod.sh # Продакшен сборка
|
||||
└── 📄 package.json # Конфигурация проекта
|
||||
|
||||
## 🎯 Назначение компонентов
|
||||
|
||||
### Основные компоненты:
|
||||
- **Dashboard** (`Dashboard.js`) - Главная панель управления
|
||||
- **ServerList** (`ServerList.js`) - Управление серверами
|
||||
- **ServerMonitoring** (`ServerMonitoring.js`) - Мониторинг серверов
|
||||
- **NetworkMonitor** (`NetworkMonitor.js`) - Мониторинг сети
|
||||
- **PasswordManager** (`PasswordManager.js`) - Менеджер паролей
|
||||
- **CommandTemplates** (`CommandTemplates.js`) - Шаблоны команд
|
||||
- **Notes** (`Notes.js`) - Система заметок
|
||||
- **Settings** (`Settings.js`) - Настройки приложения
|
||||
|
||||
### Вспомогательные компоненты:
|
||||
- **BatchOperations** - Пакетные операции
|
||||
- **NetworkTools** - Сетевые инструменты
|
||||
- **ServerHealth** - Проверка здоровья серверов
|
||||
|
||||
## 🔧 Ключевые файлы
|
||||
|
||||
### Electron файлы:
|
||||
- `public/electron.js` - Главный процесс Electron
|
||||
- `public/preload.js` - Безопасный мост между процессами
|
||||
- `public/index.html` - Базовая HTML структура
|
||||
|
||||
### Конфигурационные файлы:
|
||||
- `package.json` - Зависимости и скрипты
|
||||
- `tailwind.config.js` - Конфигурация Tailwind CSS
|
||||
- `.env` - Переменные окружения
|
||||
|
||||
## 🚀 Быстрый старт при разработке
|
||||
|
||||
# Установка зависимостей
|
||||
npm install
|
||||
|
||||
# Запуск в режиме разработки
|
||||
./build.sh dev
|
||||
|
||||
# Сборка для продакшена
|
||||
./build.sh build # Сборка Linux (.deb) + Windows (.exe)
|
||||
|
||||
# Только Linux сборка
|
||||
./build.sh build-linux
|
||||
|
||||
# Очистка сборок
|
||||
./build.sh clean
|
||||
|
||||
# !! ВАЖНО !! Настройка Wine для сборки Windows приложений
|
||||
|
||||
## Установка Wine на Ubuntu/Debian:
|
||||
sudo apt update
|
||||
sudo apt install -y wine
|
||||
|
||||
|
||||
|
||||
## 🎨 Архитектура
|
||||
|
||||
### Frontend (React):
|
||||
- **Тёмная тема** по умолчанию
|
||||
- **Tailwind CSS** для стилей
|
||||
- **Компонентный подход**
|
||||
- **Состояние через useState/useEffect**
|
||||
|
||||
### Backend (Electron):
|
||||
- **Главный процесс** - управление окнами
|
||||
- **Предзагрузка** - безопасный API мост
|
||||
- **Файловая система** - хранение данных
|
||||
|
||||
### Хранение данных:
|
||||
- **servers.json** - список серверов
|
||||
- **passwords.json** - менеджер паролей
|
||||
- **notes.json** - система заметок
|
||||
- **command-templates.json** - шаблоны команд
|
||||
@@ -0,0 +1,74 @@
|
||||
# ServerManager Pro
|
||||
|
||||
🚀 **Профессиональный инструмент для управления серверами**
|
||||
Поддержка Linux и Windows систем
|
||||
|
||||

|
||||

|
||||
|
||||
## ✨ Возможности
|
||||
|
||||
- 🖥️ **Управление серверами** - централизованное управление SSH серверами
|
||||
- 📊 **Мониторинг** - отслеживание состояния серверов в реальном времени
|
||||
- 🔐 **Менеджер паролей** - безопасное хранение учетных данных
|
||||
- ⚡ **Шаблоны команд** - быстрый доступ к часто используемым командам
|
||||
- 📝 **Система заметок** - ведение документации по серверам
|
||||
- 🌐 **Сетевые инструменты** - диагностика и мониторинг сети
|
||||
- 🔄 **Пакетные операции** - массовое выполнение команд
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### Установка на Linux:
|
||||
```bash
|
||||
# Скачайте .deb пакет из релизов
|
||||
sudo dpkg -i servermanager-pro_*.deb
|
||||
|
||||
# Или используйте скрипт установки
|
||||
chmod +x installers/install-linux.sh
|
||||
sudo ./installers/install-linux.sh
|
||||
```
|
||||
|
||||
### Установка на Windows:
|
||||
1. Скачайте `.exe` файл из релизов
|
||||
2. Запустите установщик от имени администратора
|
||||
3. Следуйте инструкциям установщика
|
||||
|
||||
## 🛠️ Для разработчиков
|
||||
|
||||
См. [PROJECT_GUIDE.md](PROJECT_GUIDE.md) для полного руководства по разработке.
|
||||
|
||||
### Основные команды:
|
||||
```bash
|
||||
./build.sh dev # Режим разработки
|
||||
./build.sh build # Сборка для производства
|
||||
./build.sh clean # Очистка сборок
|
||||
```
|
||||
|
||||
## 📁 Структура проекта
|
||||
|
||||
```
|
||||
src/components/
|
||||
├── ServerList.js # Управление серверами
|
||||
├── ServerMonitoring.js # Мониторинг серверов
|
||||
├── NetworkMonitor.js # Мониторинг сети
|
||||
├── PasswordManager.js # Менеджер паролей
|
||||
├── CommandTemplates.js # Шаблоны команд
|
||||
├── Notes.js # Система заметок
|
||||
├── Dashboard.js # Главная панель
|
||||
└── Settings.js # Настройки приложения
|
||||
```
|
||||
|
||||
## 🔧 Требования
|
||||
|
||||
- **Node.js** 16+
|
||||
- **npm** 8+
|
||||
- **Linux**: libgtk-3-0, libnss3, libxss1 (устанавливаются автоматически)
|
||||
- **Windows**: 10/11, .NET Framework 4.5+
|
||||
|
||||
## 📄 Лицензия
|
||||
|
||||
MIT License - смотрите файл LICENSE для деталей.
|
||||
|
||||
## 🤝 Поддержка
|
||||
|
||||
Нашли баг или есть предложение? Создайте issue в репозитории проекта.
|
||||
Executable
+198
@@ -0,0 +1,198 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ServerManager Pro - Production Build for Linux & Windows
|
||||
# Version: 2.0.0
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
APP_NAME="ServerManager Pro"
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
echo -e "${BLUE}=== $APP_NAME - Production Build ===${NC}"
|
||||
echo -e "${GREEN}Version: $VERSION${NC}"
|
||||
echo "======================================"
|
||||
|
||||
print_status() { echo -e "${BLUE}[$(date +%H:%M:%S)]${NC} $1"; }
|
||||
print_success() { echo -e "${GREEN}✅ $1${NC}"; }
|
||||
print_warning() { echo -e "${YELLOW}⚠️ $1${NC}"; }
|
||||
print_error() { echo -e "${RED}❌ $1${NC}"; }
|
||||
|
||||
# Check environment
|
||||
check_environment() {
|
||||
print_status "Checking environment..."
|
||||
|
||||
if [ ! -f "package.json" ]; then
|
||||
print_error "package.json not found. Run from project root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v node >/dev/null; then
|
||||
print_error "Node.js not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Environment check passed"
|
||||
}
|
||||
|
||||
# Clean builds
|
||||
clean_builds() {
|
||||
print_status "Cleaning previous builds..."
|
||||
rm -rf build dist
|
||||
print_success "Clean completed"
|
||||
}
|
||||
|
||||
# Install dependencies
|
||||
install_deps() {
|
||||
print_status "Installing dependencies..."
|
||||
|
||||
if [ ! -d "node_modules" ]; then
|
||||
npm install
|
||||
else
|
||||
npm install
|
||||
fi
|
||||
|
||||
print_success "Dependencies installed"
|
||||
}
|
||||
|
||||
# Build React app
|
||||
build_react() {
|
||||
print_status "Building React application..."
|
||||
npm run build
|
||||
|
||||
if [ ! -d "build" ]; then
|
||||
print_error "React build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "React app built"
|
||||
}
|
||||
|
||||
# Create icon if missing
|
||||
create_icon() {
|
||||
if [ ! -f "public/icon.png" ]; then
|
||||
print_status "Creating application icon..."
|
||||
|
||||
if command -v convert >/dev/null; then
|
||||
convert -size 512x512 gradient:blue-darkblue -pointsize 100 -fill white -gravity center -annotate +0+0 "SM" public/icon.png
|
||||
print_success "Icon created"
|
||||
else
|
||||
print_warning "ImageMagick not available - using default icon"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Build Linux package
|
||||
build_linux() {
|
||||
print_status "Building Linux package (.deb)..."
|
||||
|
||||
npx electron-builder --linux deb
|
||||
|
||||
if ls dist/*.deb >/dev/null 2>&1; then
|
||||
DEB_FILE=$(ls dist/*.deb | head -1)
|
||||
print_success "Linux package: $(basename $DEB_FILE) ($(du -h $DEB_FILE | cut -f1))"
|
||||
return 0
|
||||
else
|
||||
print_error "Linux package build failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Build Windows package
|
||||
build_windows() {
|
||||
print_status "Building Windows package (.exe)..."
|
||||
|
||||
npx electron-builder --win
|
||||
|
||||
if ls dist/*.exe >/dev/null 2>&1; then
|
||||
EXE_FILE=$(ls dist/*.exe | head -1)
|
||||
print_success "Windows package: $(basename $EXE_FILE) ($(du -h $EXE_FILE | cut -f1))"
|
||||
return 0
|
||||
else
|
||||
print_error "Windows package build failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate report
|
||||
generate_report() {
|
||||
echo ""
|
||||
echo -e "${GREEN}=== BUILD COMPLETE ===${NC}"
|
||||
echo "Version: $VERSION"
|
||||
echo "Build date: $(date)"
|
||||
echo ""
|
||||
|
||||
echo -e "${BLUE}Generated Packages:${NC}"
|
||||
|
||||
# Linux packages
|
||||
if ls dist/*.deb >/dev/null 2>&1; then
|
||||
echo -e " 🐧 Linux (.deb):"
|
||||
ls dist/*.deb | while read file; do
|
||||
echo " 📦 $(basename $file) ($(du -h "$file" | cut -f1))"
|
||||
done
|
||||
else
|
||||
echo -e " 🐧 Linux: ❌ Not built"
|
||||
fi
|
||||
|
||||
# Windows packages
|
||||
if ls dist/*.exe >/dev/null 2>&1; then
|
||||
echo -e " 🪟 Windows (.exe):"
|
||||
ls dist/*.exe | while read file; do
|
||||
echo " 📦 $(basename $file) ($(du -h "$file" | cut -f1))"
|
||||
done
|
||||
else
|
||||
echo -e " 🪟 Windows: ❌ Not built"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Installation Instructions:${NC}"
|
||||
echo " Linux: sudo dpkg -i dist/*.deb"
|
||||
echo " Windows: Run the .exe file as Administrator"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Next Steps:${NC}"
|
||||
echo " • Test the application on both platforms"
|
||||
echo " • Use IT"
|
||||
}
|
||||
|
||||
# Main build process
|
||||
main() {
|
||||
local platform=${1:-all}
|
||||
|
||||
case $platform in
|
||||
"linux")
|
||||
check_environment
|
||||
clean_builds
|
||||
install_deps
|
||||
create_icon
|
||||
build_react
|
||||
build_linux
|
||||
;;
|
||||
"windows")
|
||||
check_environment
|
||||
clean_builds
|
||||
install_deps
|
||||
create_icon
|
||||
build_react
|
||||
build_windows
|
||||
;;
|
||||
"all"|*)
|
||||
check_environment
|
||||
clean_builds
|
||||
install_deps
|
||||
create_icon
|
||||
build_react
|
||||
build_linux
|
||||
build_windows
|
||||
;;
|
||||
esac
|
||||
|
||||
generate_report
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ServerManager Pro - Simple Build Interface
|
||||
# For daily development use
|
||||
|
||||
set -e
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
APP_NAME="ServerManager Pro"
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
show_help() {
|
||||
echo -e "${GREEN}$APP_NAME - Build Manager${NC}"
|
||||
echo -e "Version: ${BLUE}$VERSION${NC}"
|
||||
echo ""
|
||||
echo "Usage: $0 [command]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " build Build for Linux & Windows (production)"
|
||||
echo " build-linux Build only Linux package"
|
||||
echo " build-windows Build only Windows package"
|
||||
echo " dev Start development mode"
|
||||
echo " clean Remove build files"
|
||||
echo " install-linux Install Linux package"
|
||||
echo " status Show build status"
|
||||
echo " version Show version"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 build # Production build"
|
||||
echo " $0 dev # Development mode"
|
||||
echo " $0 install-linux # Install on Linux"
|
||||
}
|
||||
|
||||
case "${1:-help}" in
|
||||
"build")
|
||||
echo -e "${GREEN}Building production packages...${NC}"
|
||||
./build-prod.sh
|
||||
;;
|
||||
"build-linux")
|
||||
echo -e "${GREEN}Building Linux package...${NC}"
|
||||
./build-prod.sh linux
|
||||
;;
|
||||
"build-windows")
|
||||
echo -e "${GREEN}Building Windows package...${NC}"
|
||||
./build-prod.sh windows
|
||||
;;
|
||||
"dev")
|
||||
echo -e "${GREEN}Starting development mode...${NC}"
|
||||
npm run electron-dev
|
||||
;;
|
||||
"clean")
|
||||
echo -e "${YELLOW}Cleaning build files...${NC}"
|
||||
rm -rf build dist
|
||||
echo -e "${GREEN}Clean complete${NC}"
|
||||
;;
|
||||
"install-linux")
|
||||
DEB_FILE=$(find dist -name "*.deb" | head -1)
|
||||
if [ -n "$DEB_FILE" ]; then
|
||||
echo -e "${GREEN}Installing $(basename $DEB_FILE)...${NC}"
|
||||
sudo dpkg -i "$DEB_FILE" || sudo apt-get install -f
|
||||
echo -e "${GREEN}Installation complete!${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}No .deb package found. Run './build.sh build' first.${NC}"
|
||||
fi
|
||||
;;
|
||||
"status")
|
||||
echo -e "${GREEN}Build Status${NC}"
|
||||
echo "Version: $VERSION"
|
||||
echo ""
|
||||
if [ -d "build" ]; then
|
||||
echo -e "React build: ${GREEN}✅ Present${NC}"
|
||||
else
|
||||
echo -e "React build: ${YELLOW}❌ Missing${NC}"
|
||||
fi
|
||||
echo -e "Packages:"
|
||||
ls dist/*.deb 2>/dev/null && echo -e " ${GREEN}🐧 Linux: ✅ Present${NC}" || echo -e " ${YELLOW}🐧 Linux: ❌ Missing${NC}"
|
||||
ls dist/*.exe 2>/dev/null && echo -e " ${GREEN}🪟 Windows: ✅ Present${NC}" || echo -e " ${YELLOW}🪟 Windows: ❌ Missing${NC}"
|
||||
;;
|
||||
"version")
|
||||
echo -e "Version: ${GREEN}$VERSION${NC}"
|
||||
;;
|
||||
"help"|"--help"|"-h")
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
echo -e "${YELLOW}Unknown command: $1${NC}"
|
||||
show_help
|
||||
;;
|
||||
esac
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ServerManager Pro - Linux Installer
|
||||
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${GREEN}ServerManager Pro - Linux Installer${NC}"
|
||||
echo "======================================"
|
||||
|
||||
# Detect architecture
|
||||
ARCH=$(uname -m)
|
||||
case $ARCH in
|
||||
x86_64) DEB_ARCH="amd64" ;;
|
||||
aarch64) DEB_ARCH="arm64" ;;
|
||||
*) echo -e "${RED}Unsupported architecture: $ARCH${NC}"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Find appropriate .deb file
|
||||
DEB_FILE=$(find ../dist -name "*${DEB_ARCH}.deb" -o -name "*.deb" | head -1)
|
||||
|
||||
if [ -z "$DEB_FILE" ]; then
|
||||
echo -e "${RED}No .deb package found${NC}"
|
||||
echo "Please build the package first: ./build-all-platforms.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "Installing: ${GREEN}$(basename $DEB_FILE)${NC}"
|
||||
echo -e "Architecture: ${GREEN}$ARCH${NC}"
|
||||
|
||||
# Install dependencies
|
||||
echo -e "${YELLOW}Installing dependencies...${NC}"
|
||||
sudo apt update
|
||||
sudo apt install -y libgtk-3-0 libnotify4 libnss3 libxss1 libxtst6 xdg-utils libatspi2.0-0 libuuid1 libappindicator3-1 libsecret-1-0
|
||||
|
||||
# Install the package
|
||||
echo -e "${YELLOW}Installing package...${NC}"
|
||||
sudo dpkg -i "$DEB_FILE" || {
|
||||
echo -e "${YELLOW}Fixing dependencies...${NC}"
|
||||
sudo apt -f install -y
|
||||
}
|
||||
|
||||
echo -e "${GREEN}Installation completed successfully!${NC}"
|
||||
echo ""
|
||||
echo -e "You can now run: ${GREEN}servermanager-pro${NC}"
|
||||
echo -e "Or find 'ServerManager Pro' in your application menu"
|
||||
Generated
+18246
File diff suppressed because it is too large
Load Diff
+104
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"name": "servermanager-pro",
|
||||
"version": "2.0.0",
|
||||
"description": "Professional Server Management Tool for Linux & Windows",
|
||||
"main": "public/electron.js",
|
||||
"homepage": "./",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"electron": "electron . --no-sandbox --disable-setuid-sandbox",
|
||||
"electron-dev": "concurrently \"npm start\" \"wait-on http://localhost:3000 && electron . --no-sandbox --disable-setuid-sandbox\"",
|
||||
"dist": "npm run build && electron-builder",
|
||||
"dist:linux": "npm run build && electron-builder --linux deb",
|
||||
"dist:windows": "npm run build && electron-builder --win",
|
||||
"dist:all": "npm run build && electron-builder -lw"
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "^7.6.0",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"wait-on": "^7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^22.0.0",
|
||||
"electron-builder": "^23.6.0"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.servermanager.pro",
|
||||
"productName": "ServerManager Pro",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"files": [
|
||||
"build/**/*",
|
||||
"public/electron.js",
|
||||
"public/preload.js",
|
||||
"public/icon.png"
|
||||
],
|
||||
"linux": {
|
||||
"target": [
|
||||
{
|
||||
"target": "deb",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"category": "Development",
|
||||
"desktop": {
|
||||
"Name": "ServerManager Pro",
|
||||
"Comment": "Professional Server Management Tool",
|
||||
"Categories": "Development;"
|
||||
},
|
||||
"icon": "public/icon.png"
|
||||
},
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "public/icon.png"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
}
|
||||
},
|
||||
"author": {
|
||||
"name": "ServerManager Team",
|
||||
"email": "support@servermanager.com"
|
||||
},
|
||||
"keywords": [
|
||||
"server",
|
||||
"management",
|
||||
"ssh",
|
||||
"network",
|
||||
"monitoring",
|
||||
"linux",
|
||||
"windows"
|
||||
],
|
||||
"license": "MIT",
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
const { app, BrowserWindow, ipcMain, shell, dialog, clipboard } = require('electron');
|
||||
const path = require('path');
|
||||
const isDev = require('electron-is-dev');
|
||||
const { exec } = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
// КРИТИЧЕСКИ ВАЖНО ДЛЯ LINUX - исправление песочницы
|
||||
app.commandLine.appendSwitch('--no-sandbox');
|
||||
app.commandLine.appendSwitch('--disable-setuid-sandbox');
|
||||
app.commandLine.appendSwitch('--disable-gpu');
|
||||
|
||||
// Дополнительные флаги для Linux
|
||||
if (process.platform === 'linux') {
|
||||
app.commandLine.appendSwitch('--no-zygote');
|
||||
app.commandLine.appendSwitch('--disable-dev-shm-usage');
|
||||
}
|
||||
|
||||
app.disableHardwareAcceleration();
|
||||
|
||||
let mainWindow;
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
webSecurity: false,
|
||||
},
|
||||
title: 'ServerManager Pro',
|
||||
icon: path.join(__dirname, 'icon.png'),
|
||||
show: false,
|
||||
titleBarStyle: 'default'
|
||||
});
|
||||
|
||||
const startUrl = isDev
|
||||
? 'http://localhost:3000'
|
||||
: `file://${path.join(__dirname, '../build/index.html')}`;
|
||||
|
||||
mainWindow.loadURL(startUrl);
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
// Открываем DevTools только в режиме разработки
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================
|
||||
|
||||
function ensureUserDataDir() {
|
||||
const userDataPath = app.getPath('userData');
|
||||
if (!fs.existsSync(userDataPath)) {
|
||||
fs.mkdirSync(userDataPath, { recursive: true });
|
||||
}
|
||||
return userDataPath;
|
||||
}
|
||||
|
||||
ipcMain.handle('connect-ssh', async (event, server) => {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const { ip, port = '22', username = 'root', password, sshKey } = server;
|
||||
|
||||
// Очищаем IP от лишних символов
|
||||
const cleanIp = ip.trim().replace(/[^0-9.:]/g, '');
|
||||
|
||||
let sshCommand = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${username}@${cleanIp} -p ${port}`;
|
||||
|
||||
if (sshKey) {
|
||||
const tempKeyPath = `/tmp/ssh_key_${Date.now()}`;
|
||||
fs.writeFileSync(tempKeyPath, sshKey);
|
||||
fs.chmodSync(tempKeyPath, 0o600);
|
||||
sshCommand = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -i ${tempKeyPath} ${username}@${cleanIp} -p ${port}`;
|
||||
}
|
||||
|
||||
const terminals = [
|
||||
{ cmd: 'gnome-terminal --', test: 'gnome-terminal' },
|
||||
{ cmd: 'konsole -e', test: 'konsole' },
|
||||
{ cmd: 'xfce4-terminal -x', test: 'xfce4-terminal' },
|
||||
{ cmd: 'mate-terminal -x', test: 'mate-terminal' },
|
||||
{ cmd: 'xterm -e', test: 'xterm' }
|
||||
];
|
||||
|
||||
const tryTerminal = (index) => {
|
||||
if (index >= terminals.length) {
|
||||
// Если терминал не найден, показываем команду для копирования
|
||||
const message = `Терминал не найден\n\nКоманда SSH:\n${sshCommand}\n\nПароль: ${password || 'не установлен'}\n\nСкопируйте команду и выполните вручную в терминале.`;
|
||||
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'info',
|
||||
title: 'SSH Подключение',
|
||||
message: 'Терминал не найден',
|
||||
detail: message
|
||||
}).then(() => {
|
||||
resolve({ success: true });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const terminal = terminals[index];
|
||||
|
||||
// Проверяем, доступен ли терминал
|
||||
exec(`which ${terminal.test}`, (error) => {
|
||||
if (error) {
|
||||
tryTerminal(index + 1);
|
||||
} else {
|
||||
console.log('Using terminal:', terminal.cmd);
|
||||
const fullCommand = `${terminal.cmd} bash -c "${sshCommand}; echo 'Нажмите Enter для закрытия...'; read"`;
|
||||
|
||||
exec(fullCommand, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error('SSH terminal error:', error);
|
||||
// Показываем ошибку пользователю
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'error',
|
||||
title: 'Ошибка SSH',
|
||||
message: `Не удалось подключиться: ${error.message}`,
|
||||
detail: stderr || 'Проверьте параметры подключения'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resolve({ success: true });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tryTerminal(0);
|
||||
|
||||
} catch (error) {
|
||||
console.error('SSH connection error:', error);
|
||||
resolve({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== ВЕБ-ИНТЕРФЕЙС ====================
|
||||
ipcMain.handle('open-web-interface', async (event, server) => {
|
||||
try {
|
||||
const { ip, webPort = '51821' } = server;
|
||||
|
||||
// Очищаем IP
|
||||
const cleanIp = ip.trim().replace(/[^0-9.:]/g, '');
|
||||
const url = `http://${cleanIp}:${webPort}`;
|
||||
|
||||
console.log('Opening web interface:', url);
|
||||
|
||||
// Проверяем доступность URL перед открытием
|
||||
const { exec } = require('child_process');
|
||||
|
||||
// Проверяем доступность с помощью curl
|
||||
exec(`curl -s --head --connect-timeout 3 ${url}`, (error) => {
|
||||
if (error) {
|
||||
// Если недоступно, показываем предупреждение
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'warning',
|
||||
title: 'Веб-интерфейс может быть недоступен',
|
||||
message: 'Сервер может не отвечать',
|
||||
detail: `URL: ${url}\n\nПроверьте:\n• Доступность сервера\n• Правильность порта\n• Запущен ли веб-сервис`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await shell.openExternal(url);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error opening web interface:', error);
|
||||
|
||||
const { ip, webPort = '51821' } = server;
|
||||
const cleanIp = ip.trim().replace(/[^0-9.:]/g, '');
|
||||
const url = `http://${cleanIp}:${webPort}`;
|
||||
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'info',
|
||||
title: 'Веб-интерфейс',
|
||||
message: 'Откройте в браузере',
|
||||
detail: `URL: ${url}\n\nЕсли не открывается автоматически, скопируйте ссылку в браузер.`
|
||||
});
|
||||
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== УПРАВЛЕНИЕ ДАННЫМИ ====================
|
||||
|
||||
// Серверы
|
||||
ipcMain.handle('load-servers', async () => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const serversPath = path.join(userDataPath, 'servers.json');
|
||||
if (fs.existsSync(serversPath)) {
|
||||
const data = fs.readFileSync(serversPath, 'utf8');
|
||||
return { success: true, servers: JSON.parse(data) };
|
||||
}
|
||||
return { success: true, servers: [] };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message, servers: [] };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('save-servers', async (event, servers) => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const serversPath = path.join(userDataPath, 'servers.json');
|
||||
fs.writeFileSync(serversPath, JSON.stringify(servers, null, 2));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Пароли
|
||||
ipcMain.handle('load-passwords', async () => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const passwordsPath = path.join(userDataPath, 'passwords.json');
|
||||
if (fs.existsSync(passwordsPath)) {
|
||||
const data = fs.readFileSync(passwordsPath, 'utf8');
|
||||
return { success: true, passwords: JSON.parse(data) };
|
||||
}
|
||||
return { success: true, passwords: [] };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message, passwords: [] };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('save-passwords', async (event, passwords) => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const passwordsPath = path.join(userDataPath, 'passwords.json');
|
||||
fs.writeFileSync(passwordsPath, JSON.stringify(passwords, null, 2));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Шаблоны команд
|
||||
ipcMain.handle('load-command-templates', async () => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const templatesPath = path.join(userDataPath, 'command-templates.json');
|
||||
if (fs.existsSync(templatesPath)) {
|
||||
const data = fs.readFileSync(templatesPath, 'utf8');
|
||||
return { success: true, templates: JSON.parse(data) };
|
||||
}
|
||||
return { success: true, templates: [] };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message, templates: [] };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('save-command-templates', async (event, templates) => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const templatesPath = path.join(userDataPath, 'command-templates.json');
|
||||
fs.writeFileSync(templatesPath, JSON.stringify(templates, null, 2));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Базовые утилиты
|
||||
ipcMain.handle('copy-to-clipboard', async (event, text) => {
|
||||
try {
|
||||
clipboard.writeText(text);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Экспорт/импорт данных
|
||||
ipcMain.handle('export-data', async (event, data) => {
|
||||
const result = await dialog.showSaveDialog(mainWindow, {
|
||||
title: 'Экспорт данных',
|
||||
defaultPath: `servermanager-backup-${new Date().toISOString().split('T')[0]}.json`,
|
||||
filters: [{ name: 'JSON', extensions: ['json'] }]
|
||||
});
|
||||
|
||||
if (result.canceled) return { success: false, error: 'Отменено' };
|
||||
|
||||
try {
|
||||
fs.writeFileSync(result.filePath, JSON.stringify(data, null, 2));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('import-data', async (event) => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Импорт данных',
|
||||
filters: [{ name: 'JSON', extensions: ['json'] }],
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
if (result.canceled) return { success: false, error: 'Отменено' };
|
||||
|
||||
try {
|
||||
const data = fs.readFileSync(result.filePaths[0], 'utf8');
|
||||
return { success: true, data: JSON.parse(data) };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Добавьте после других ipcMain.handle
|
||||
|
||||
// Уведомления
|
||||
ipcMain.handle('show-notification', async (event, title, body) => {
|
||||
try {
|
||||
// Используем встроенные уведомления Electron
|
||||
const notification = {
|
||||
title: title,
|
||||
body: body,
|
||||
silent: true
|
||||
};
|
||||
|
||||
// Показываем уведомление в главном процессе
|
||||
const notif = new Notification(notification);
|
||||
notif.show();
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Notification error:', error);
|
||||
// Если уведомления не работают, просто логируем
|
||||
console.log(`Уведомление: ${title} - ${body}`);
|
||||
return { success: true };
|
||||
}
|
||||
});
|
||||
// Логирование действий
|
||||
ipcMain.handle('log-action', async (event, action) => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const logPath = path.join(userDataPath, 'actions.log');
|
||||
const timestamp = new Date().toISOString();
|
||||
const logEntry = `${timestamp}: ${action}\n`;
|
||||
fs.appendFileSync(logPath, logEntry);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Ping функция для проверки серверов
|
||||
ipcMain.handle('ping-server', async (event, server) => {
|
||||
try {
|
||||
const { ip } = server;
|
||||
const cleanIp = ip.trim().replace(/[^0-9.:]/g, '');
|
||||
|
||||
// Используем системный ping
|
||||
const { exec } = require('child_process');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec(`ping -c 2 -W 1 ${cleanIp}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
resolve({
|
||||
success: true,
|
||||
online: false,
|
||||
responseTime: null,
|
||||
packetLoss: 100
|
||||
});
|
||||
} else {
|
||||
// Парсим вывод ping для получения времени ответа
|
||||
const timeMatch = stdout.match(/time=(\d+\.?\d*) ms/);
|
||||
const responseTime = timeMatch ? parseFloat(timeMatch[1]) : null;
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
online: true,
|
||||
responseTime: responseTime,
|
||||
packetLoss: 0
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
online: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== СЕТЕВЫЕ ДАННЫЕ ====================
|
||||
|
||||
// Получение сетевых адаптеров
|
||||
ipcMain.handle('get-network-adapters', async () => {
|
||||
try {
|
||||
const { networkInterfaces } = require('os');
|
||||
const interfaces = networkInterfaces();
|
||||
const adapters = [];
|
||||
|
||||
for (const [name, details] of Object.entries(interfaces)) {
|
||||
for (const detail of details) {
|
||||
if (detail.family === 'IPv4' && !detail.internal) {
|
||||
adapters.push({
|
||||
name: name,
|
||||
description: `${name} Network Interface`,
|
||||
status: 'Up',
|
||||
ipAddress: detail.address,
|
||||
macAddress: detail.mac,
|
||||
type: getInterfaceType(name)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, adapters };
|
||||
} catch (error) {
|
||||
console.error('Error getting network adapters:', error);
|
||||
return { success: false, error: error.message, adapters: [] };
|
||||
}
|
||||
});
|
||||
|
||||
// Получение VPN подключений
|
||||
ipcMain.handle('get-vpn-connections', async () => {
|
||||
try {
|
||||
const connections = [];
|
||||
|
||||
// Проверяем WireGuard
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
const wireguardResult = execSync('ip link show type wireguard 2>/dev/null || echo ""', { encoding: 'utf8' });
|
||||
if (wireguardResult && wireguardResult.includes('wireguard')) {
|
||||
connections.push({
|
||||
name: 'WireGuard VPN',
|
||||
interface: 'wg0',
|
||||
status: 'Connected',
|
||||
description: 'WireGuard VPN Tunnel'
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// WireGuard не установлен или нет интерфейсов
|
||||
}
|
||||
|
||||
// Проверяем OpenVPN
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
const openvpnResult = execSync('ps aux | grep openvpn | grep -v grep || echo ""', { encoding: 'utf8' });
|
||||
if (openvpnResult && openvpnResult.includes('openvpn')) {
|
||||
connections.push({
|
||||
name: 'OpenVPN',
|
||||
interface: 'tun0',
|
||||
status: 'Connected',
|
||||
description: 'OpenVPN Connection'
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// OpenVPN не запущен
|
||||
}
|
||||
|
||||
return { success: true, connections };
|
||||
} catch (error) {
|
||||
console.error('Error getting VPN connections:', error);
|
||||
return { success: false, error: error.message, connections: [] };
|
||||
}
|
||||
});
|
||||
|
||||
// Вспомогательная функция для определения типа интерфейса
|
||||
function getInterfaceType(name) {
|
||||
if (name.includes('wl') || name.includes('wlan') || name.includes('wifi')) {
|
||||
return 'Wi-Fi';
|
||||
} else if (name.includes('eth') || name.includes('enp') || name.includes('ens')) {
|
||||
return 'Ethernet';
|
||||
} else if (name.includes('tun') || name.includes('tap')) {
|
||||
return 'VPN';
|
||||
} else if (name.includes('lo')) {
|
||||
return 'Loopback';
|
||||
} else {
|
||||
return 'Ethernet';
|
||||
}
|
||||
}
|
||||
|
||||
// Заметки
|
||||
ipcMain.handle('load-notes', async () => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const notesPath = path.join(userDataPath, 'notes.json');
|
||||
if (fs.existsSync(notesPath)) {
|
||||
const data = fs.readFileSync(notesPath, 'utf8');
|
||||
return { success: true, notes: JSON.parse(data) };
|
||||
}
|
||||
return { success: true, notes: [] };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message, notes: [] };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('save-notes', async (event, notes) => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const notesPath = path.join(userDataPath, 'notes.json');
|
||||
fs.writeFileSync(notesPath, JSON.stringify(notes, null, 2));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('load-note-folders', async () => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const foldersPath = path.join(userDataPath, 'note-folders.json');
|
||||
if (fs.existsSync(foldersPath)) {
|
||||
const data = fs.readFileSync(foldersPath, 'utf8');
|
||||
return { success: true, folders: JSON.parse(data) };
|
||||
}
|
||||
return { success: true, folders: [] };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message, folders: [] };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('save-note-folders', async (event, folders) => {
|
||||
try {
|
||||
const userDataPath = ensureUserDataDir();
|
||||
const foldersPath = path.join(userDataPath, 'note-folders.json');
|
||||
fs.writeFileSync(foldersPath, JSON.stringify(folders, null, 2));
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#0ea5e9;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0369a1;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="80" fill="url(#grad)"/>
|
||||
<text x="256" y="280" font-family="Arial, sans-serif" font-size="180" font-weight="bold"
|
||||
text-anchor="middle" fill="white">SM</text>
|
||||
<text x="256" y="380" font-family="Arial, sans-serif" font-size="40"
|
||||
text-anchor="middle" fill="white">Pro</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 673 B |
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="WireGuard Manager - управление VPN серверами"
|
||||
/>
|
||||
<title>WireGuard Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Основные функции
|
||||
loadServers: () => ipcRenderer.invoke('load-servers'),
|
||||
saveServers: (servers) => ipcRenderer.invoke('save-servers', servers),
|
||||
connectSSH: (server) => ipcRenderer.invoke('connect-ssh', server),
|
||||
openWebInterface: (server) => ipcRenderer.invoke('open-web-interface', server),
|
||||
|
||||
// Пароли
|
||||
loadPasswords: () => ipcRenderer.invoke('load-passwords'),
|
||||
savePasswords: (passwords) => ipcRenderer.invoke('save-passwords', passwords),
|
||||
|
||||
// Шаблоны команд
|
||||
loadCommandTemplates: () => ipcRenderer.invoke('load-command-templates'),
|
||||
saveCommandTemplates: (templates) => ipcRenderer.invoke('save-command-templates', templates),
|
||||
|
||||
// Утилиты
|
||||
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
|
||||
exportData: (data) => ipcRenderer.invoke('export-data', data),
|
||||
importData: () => ipcRenderer.invoke('import-data'),
|
||||
|
||||
// Новые функции
|
||||
showNotification: (title, body) => ipcRenderer.invoke('show-notification', title, body),
|
||||
logAction: (action) => ipcRenderer.invoke('log-action', action),
|
||||
|
||||
// Мониторинг сети
|
||||
getNetworkAdapters: () => ipcRenderer.invoke('get-network-adapters'),
|
||||
getVpnConnections: () => ipcRenderer.invoke('get-vpn-connections'),
|
||||
|
||||
// Ping функция
|
||||
pingServer: (server) => ipcRenderer.invoke('ping-server', server),
|
||||
|
||||
// Заметки
|
||||
loadNotes: () => ipcRenderer.invoke('load-notes'),
|
||||
saveNotes: (notes) => ipcRenderer.invoke('save-notes', notes),
|
||||
loadNoteFolders: () => ipcRenderer.invoke('load-note-folders'),
|
||||
saveNoteFolders: (folders) => ipcRenderer.invoke('save-note-folders', folders),
|
||||
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Version=2.0.0
|
||||
Name=ServerManager Pro
|
||||
Comment=Professional Server Management Tool
|
||||
Exec=/opt/ServerManager Pro/servermanager-pro
|
||||
Icon=/opt/ServerManager Pro/icon.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Development;Network;
|
||||
StartupWMClass=ServerManager Pro
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Create a simple icon if none exists
|
||||
mkdir -p public
|
||||
cd public
|
||||
|
||||
# Create a simple SVG icon if icon.png doesn't exist
|
||||
if [ ! -f "icon.png" ]; then
|
||||
echo "Creating default icon..."
|
||||
|
||||
# Create SVG icon
|
||||
cat > icon.svg << 'EOF'
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#0ea5e9;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0369a1;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="80" fill="url(#grad)"/>
|
||||
<text x="256" y="280" font-family="Arial, sans-serif" font-size="180" font-weight="bold"
|
||||
text-anchor="middle" fill="white">SM</text>
|
||||
<text x="256" y="380" font-family="Arial, sans-serif" font-size="40"
|
||||
text-anchor="middle" fill="white">Pro</text>
|
||||
</svg>
|
||||
EOF
|
||||
|
||||
# Convert SVG to PNG (requires imagemagick)
|
||||
if command -v convert &> /dev/null; then
|
||||
convert -background none -resize 512x512 icon.svg icon.png
|
||||
echo "Icon created: public/icon.png"
|
||||
else
|
||||
echo "Install imagemagick to convert SVG to PNG: sudo apt install imagemagick"
|
||||
echo "Using SVG icon for now..."
|
||||
fi
|
||||
fi
|
||||
|
||||
cd ..
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ServerList from './components/ServerList';
|
||||
import NetworkMonitor from './components/NetworkMonitor';
|
||||
import PasswordManager from './components/PasswordManager';
|
||||
import ServerMonitoring from './components/ServerMonitoring';
|
||||
import CommandTemplates from './components/CommandTemplates';
|
||||
import Settings from './components/Settings';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import Notes from './components/Notes';
|
||||
|
||||
const App = () => {
|
||||
const [activeTab, setActiveTab] = useState('dashboard');
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
// Всегда темная тема
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.add('dark');
|
||||
}, []);
|
||||
|
||||
// Закрываем сайдбар при изменении размера окна на десктоп
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 768) {
|
||||
setIsSidebarOpen(true);
|
||||
} else {
|
||||
setIsSidebarOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize(); // Вызываем сразу при загрузке
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'servers': return <ServerList />;
|
||||
case 'monitoring': return <ServerMonitoring />;
|
||||
case 'network': return <NetworkMonitor />;
|
||||
case 'templates': return <CommandTemplates />;
|
||||
case 'passwords': return <PasswordManager />;
|
||||
case 'dashboard': return <Dashboard />;
|
||||
case 'settings': return <Settings />;
|
||||
case 'notes': return <Notes />;
|
||||
default: return <Dashboard />;
|
||||
}
|
||||
};
|
||||
|
||||
const getTabStyle = (tabName) => {
|
||||
return `w-full text-left p-3 rounded-lg transition-all flex items-center space-x-3 text-sm ${
|
||||
activeTab === tabName
|
||||
? 'bg-primary-600 text-white shadow-lg'
|
||||
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
|
||||
}`;
|
||||
};
|
||||
|
||||
const handleTabClick = (tabName) => {
|
||||
setActiveTab(tabName);
|
||||
if (window.innerWidth < 768) {
|
||||
setIsSidebarOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-900 dark:bg-gray-900">
|
||||
{/* Мобильное меню */}
|
||||
<div className="md:hidden fixed top-4 left-4 z-50">
|
||||
<button
|
||||
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||
className="p-2 bg-gray-800 rounded-lg text-white shadow-lg"
|
||||
>
|
||||
{isSidebarOpen ? '✕' : '☰'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Боковая панель */}
|
||||
<div className={`
|
||||
fixed md:relative inset-y-0 left-0 z-40
|
||||
w-64 bg-gray-800 text-white flex flex-col shadow-2xl
|
||||
transform transition-transform duration-300 ease-in-out
|
||||
${isSidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
|
||||
`}>
|
||||
<div className="p-6 border-b border-gray-700">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-primary-500 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-lg">SM</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold">ServerManager</h1>
|
||||
<p className="text-gray-400 text-sm">v2.0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4">
|
||||
<ul className="space-y-1">
|
||||
<li>
|
||||
<button onClick={() => handleTabClick('dashboard')} className={getTabStyle('dashboard')}>
|
||||
<span className="text-lg">📊</span>
|
||||
<span>Дашборд</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => handleTabClick('servers')} className={getTabStyle('servers')}>
|
||||
<span className="text-lg">🖥️</span>
|
||||
<span>Серверы</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => handleTabClick('monitoring')} className={getTabStyle('monitoring')}>
|
||||
<span className="text-lg">📊</span>
|
||||
<span>Мониторинг</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => handleTabClick('network')} className={getTabStyle('network')}>
|
||||
<span className="text-lg">🌐</span>
|
||||
<span>Сеть</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => handleTabClick('templates')} className={getTabStyle('templates')}>
|
||||
<span className="text-lg">⚡</span>
|
||||
<span>Команды</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => handleTabClick('passwords')} className={getTabStyle('passwords')}>
|
||||
<span className="text-lg">🔐</span>
|
||||
<span>Пароли</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => handleTabClick('notes')} className={getTabStyle('notes')}>
|
||||
<span className="text-lg">📝</span>
|
||||
<span>Заметки</span>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => handleTabClick('settings')} className={getTabStyle('settings')}>
|
||||
<span className="text-lg">⚙️</span>
|
||||
<span>Настройки</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-gray-700">
|
||||
<div className="text-xs text-gray-400 text-center">
|
||||
ServerManager © 2024
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overlay для мобильных */}
|
||||
{isSidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-30 md:hidden"
|
||||
onClick={() => setIsSidebarOpen(false)}
|
||||
></div>
|
||||
)}
|
||||
|
||||
{/* Основной контент */}
|
||||
<div className="flex-1 overflow-auto md:ml-0">
|
||||
<div className="md:hidden p-4 bg-gray-800 border-b border-gray-700">
|
||||
<h1 className="text-lg font-bold text-white">
|
||||
{activeTab === 'dashboard' && 'Дашборд'}
|
||||
{activeTab === 'servers' && 'Серверы'}
|
||||
{activeTab === 'monitoring' && 'Мониторинг'}
|
||||
{activeTab === 'network' && 'Сеть'}
|
||||
{activeTab === 'templates' && 'Шаблоны команд'}
|
||||
{activeTab === 'passwords' && 'Менеджер паролей'}
|
||||
{activeTab === 'notes' && 'Заметки'}
|
||||
{activeTab === 'settings' && 'Настройки'}
|
||||
</h1>
|
||||
</div>
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -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;
|
||||
@@ -0,0 +1,55 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
/* Убираем стандартные стили кнопок */
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Стили для скроллбара */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* Темная тема для скроллбара */
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -0,0 +1,29 @@
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
"./public/index.html"
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
200: '#bae6fd',
|
||||
300: '#7dd3fc',
|
||||
400: '#38bdf8',
|
||||
500: '#0ea5e9',
|
||||
600: '#0284c7',
|
||||
700: '#0369a1',
|
||||
800: '#075985',
|
||||
900: '#0c4a6e',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
Reference in New Issue
Block a user