Files
NeveTimePanel/frontend/src/App.jsx
arkonsadter d188cec1f0
All checks were successful
continuous-integration/drone/push Build is passing
Added Daemon system and fixed interface
2026-01-16 18:56:21 +06:00

588 lines
23 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import {
Server, Play, Square, Terminal, FolderOpen, Settings, Plus,
Users as UsersIcon, LogOut, Menu, X, MessageSquare, UserCircle,
Shield, Activity, HardDrive, Cpu, BarChart3, Home
} from 'lucide-react';
import Console from './components/Console';
import FileManager from './components/FileManager';
import Stats from './components/Stats';
import ServerSettings from './components/ServerSettings';
import CreateServerModal from './components/CreateServerModal';
import Users from './components/Users';
import UserManagement from './components/UserManagement';
import Tickets from './components/Tickets';
import Profile from './components/Profile';
import Daemons from './components/Daemons';
import Auth from './components/Auth';
import ErrorBoundary from './components/ErrorBoundary';
import NotificationSystem, { notify } from './components/NotificationSystem';
import axios from 'axios';
import { API_URL } from './config';
function App() {
const [token, setToken] = useState(localStorage.getItem('token'));
const [user, setUser] = useState(null);
const [servers, setServers] = useState([]);
const [selectedServer, setSelectedServer] = useState(null);
const [activeTab, setActiveTab] = useState('console');
const [showCreateModal, setShowCreateModal] = useState(false);
const [showUserManagement, setShowUserManagement] = useState(false);
const [showDaemons, setShowDaemons] = useState(false);
const [showTickets, setShowTickets] = useState(false);
const [showProfile, setShowProfile] = useState(false);
const [connectionError, setConnectionError] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [currentView, setCurrentView] = useState('dashboard');
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const callbackToken = urlParams.get('token');
const callbackUsername = urlParams.get('username');
if (callbackToken && callbackUsername) {
localStorage.setItem('token', callbackToken);
setToken(callbackToken);
setUser({ username: callbackUsername });
window.history.replaceState({}, document.title, window.location.pathname);
return;
}
if (token) {
loadUser();
loadServers();
const interval = setInterval(loadServers, 5000);
return () => clearInterval(interval);
}
}, [token]);
const loadUser = async () => {
try {
const { data } = await axios.get(`${API_URL}/api/auth/me`, {
headers: { Authorization: `Bearer ${token}` }
});
setUser(data);
} catch (error) {
console.error('Ошибка загрузки пользователя:', error);
handleLogout();
}
};
const loadServers = async () => {
try {
const { data } = await axios.get(`${API_URL}/api/servers`, {
headers: { Authorization: `Bearer ${token}` }
});
setServers(data);
setConnectionError(false);
} catch (error) {
console.error('Ошибка загрузки серверов:', error);
if (error.response?.status === 401) {
handleLogout();
} else {
setConnectionError(true);
}
}
};
const handleLogin = async (username, password, isLogin) => {
const endpoint = isLogin ? '/api/auth/login' : '/api/auth/register';
const { data } = await axios.post(`${API_URL}${endpoint}`, {
username,
password
});
localStorage.setItem('token', data.access_token);
setToken(data.access_token);
setUser({ username: data.username, role: data.role });
notify.success(`Добро пожаловать, ${data.username}!`);
};
const handleLogout = () => {
localStorage.removeItem('token');
setToken(null);
setUser(null);
setServers([]);
setSelectedServer(null);
notify.info('Вы вышли из системы');
};
const handleServerAction = async (serverName, action) => {
try {
await axios.post(
`${API_URL}/api/servers/${serverName}/${action}`,
{},
{ headers: { Authorization: `Bearer ${token}` } }
);
notify.success(`Сервер ${action === 'start' ? 'запущен' : 'остановлен'}`);
loadServers();
} catch (error) {
notify.error(`Ошибка: ${error.response?.data?.detail || error.message}`);
}
};
if (!token) {
return (
<ErrorBoundary>
<Auth onLogin={handleLogin} />
<NotificationSystem />
</ErrorBoundary>
);
}
return (
<ErrorBoundary>
<div className="flex h-screen bg-dark-900 overflow-hidden">
{/* Sidebar */}
<aside className={`${sidebarOpen ? 'w-64' : 'w-20'} bg-dark-850 border-r border-dark-700 transition-all duration-300 flex flex-col`}>
{/* Logo */}
<div className="h-16 flex items-center justify-between px-4 border-b border-dark-700">
{sidebarOpen && (
<div className="flex items-center gap-2">
<Server className="w-8 h-8 text-primary-500" />
<span className="text-xl font-bold bg-gradient-to-r from-primary-400 to-blue-500 bg-clip-text text-transparent">
MC Panel
</span>
</div>
)}
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="btn-icon"
>
{sidebarOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
</div>
{/* Navigation */}
<nav className="flex-1 p-4 space-y-2 overflow-y-auto">
<button
onClick={() => {
setCurrentView('dashboard');
setSelectedServer(null);
}}
className={currentView === 'dashboard' ? 'sidebar-item-active w-full' : 'sidebar-item w-full'}
>
<Home className="w-5 h-5 flex-shrink-0" />
{sidebarOpen && <span>Главная</span>}
</button>
{sidebarOpen && (
<div className="pt-4 pb-2">
<div className="flex items-center justify-between px-4 mb-2">
<span className="text-xs font-semibold text-gray-500 uppercase">Серверы</span>
<button
onClick={() => setShowCreateModal(true)}
className="p-1 rounded hover:bg-dark-700 text-primary-500"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
)}
{servers.map((server) => (
<button
key={server.name}
onClick={() => {
setSelectedServer(server);
setCurrentView('server');
setActiveTab('console');
}}
className={selectedServer?.name === server.name ? 'sidebar-item-active w-full' : 'sidebar-item w-full'}
>
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${
server.status === 'running' ? 'bg-green-500 shadow-glow' : 'bg-gray-600'
}`} />
{sidebarOpen && (
<div className="flex-1 text-left truncate">
<div className="text-sm font-medium">{server.displayName}</div>
<div className="text-xs text-gray-500">{server.status === 'running' ? 'Запущен' : 'Остановлен'}</div>
</div>
)}
</button>
))}
{sidebarOpen && servers.length === 0 && (
<div className="text-center py-8 text-gray-500 text-sm">
<Server className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>Нет серверов</p>
<button
onClick={() => setShowCreateModal(true)}
className="btn-primary mt-2 text-xs"
>
Создать сервер
</button>
</div>
)}
</nav>
{/* Bottom section */}
<div className="p-4 border-t border-dark-700 space-y-2">
{(user?.role === 'owner' || user?.role === 'admin') && (
<>
<button
onClick={() => {
setShowUserManagement(true);
setCurrentView('management');
}}
className="sidebar-item w-full"
>
<Shield className="w-5 h-5 flex-shrink-0 text-yellow-500" />
{sidebarOpen && <span>Управление</span>}
</button>
<button
onClick={() => {
setShowDaemons(true);
setCurrentView('daemons');
}}
className="sidebar-item w-full"
>
<Server className="w-5 h-5 flex-shrink-0 text-blue-500" />
{sidebarOpen && <span>Демоны</span>}
</button>
</>
)}
<button
onClick={() => {
setShowTickets(true);
setCurrentView('tickets');
}}
className="sidebar-item w-full"
>
<MessageSquare className="w-5 h-5 flex-shrink-0" />
{sidebarOpen && <span>Тикеты</span>}
</button>
<button
onClick={() => {
setShowProfile(true);
setCurrentView('profile');
}}
className="sidebar-item w-full"
>
<UserCircle className="w-5 h-5 flex-shrink-0" />
{sidebarOpen && <span>Профиль</span>}
</button>
<button
onClick={handleLogout}
className="sidebar-item w-full text-red-400 hover:text-red-300 hover:bg-red-600/10"
>
<LogOut className="w-5 h-5 flex-shrink-0" />
{sidebarOpen && <span>Выход</span>}
</button>
{sidebarOpen && user && (
<div className="pt-2 border-t border-dark-700">
<div className="text-xs text-gray-500">
<div className="font-medium text-gray-300">{user.username}</div>
<div className="capitalize">{user.role}</div>
</div>
</div>
)}
</div>
</aside>
{/* Main content */}
<main className="flex-1 flex flex-col overflow-hidden">
{/* Header */}
<header className="h-16 bg-dark-850 border-b border-dark-700 flex items-center justify-between px-6">
<div>
<h1 className="text-2xl font-bold text-gray-100">
{currentView === 'dashboard' && 'Панель управления'}
{currentView === 'server' && selectedServer?.displayName}
{currentView === 'management' && 'Управление пользователями'}
{currentView === 'daemons' && 'Управление демонами'}
{currentView === 'tickets' && 'Тикеты поддержки'}
{currentView === 'profile' && 'Профиль'}
</h1>
{currentView === 'server' && selectedServer && (
<p className="text-sm text-gray-500">
{selectedServer.name} {selectedServer.status === 'running' ? 'Запущен' : 'Остановлен'}
</p>
)}
</div>
{currentView === 'server' && selectedServer && (
<div className="flex items-center gap-2">
{selectedServer.status === 'stopped' ? (
<button
onClick={() => handleServerAction(selectedServer.name, 'start')}
className="btn-success flex items-center gap-2"
>
<Play className="w-4 h-4" />
Запустить
</button>
) : (
<button
onClick={() => handleServerAction(selectedServer.name, 'stop')}
className="btn-danger flex items-center gap-2"
>
<Square className="w-4 h-4" />
Остановить
</button>
)}
</div>
)}
</header>
{/* Content area */}
<div className="flex-1 overflow-auto p-6">
{connectionError && (
<div className="card p-4 mb-4 bg-red-600/10 border-red-600/30">
<p className="text-red-400"> Ошибка подключения к серверу</p>
</div>
)}
{currentView === 'dashboard' && (
<div className="space-y-6 animate-fade-in">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="stat-card">
<div className="stat-icon">
<Server className="w-6 h-6" />
</div>
<div>
<div className="text-2xl font-bold">{servers.length}</div>
<div className="text-sm text-gray-500">Всего серверов</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon bg-green-600/20 text-green-400">
<Activity className="w-6 h-6" />
</div>
<div>
<div className="text-2xl font-bold text-green-400">
{servers.filter(s => s.status === 'running').length}
</div>
<div className="text-sm text-gray-500">Запущено</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon bg-gray-600/20 text-gray-400">
<Square className="w-6 h-6" />
</div>
<div>
<div className="text-2xl font-bold text-gray-400">
{servers.filter(s => s.status === 'stopped').length}
</div>
<div className="text-sm text-gray-500">Остановлено</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon bg-purple-600/20 text-purple-400">
<UserCircle className="w-6 h-6" />
</div>
<div>
<div className="text-2xl font-bold text-purple-400">{user?.username}</div>
<div className="text-sm text-gray-500 capitalize">{user?.role}</div>
</div>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold">Ваши серверы</h2>
<button
onClick={() => setShowCreateModal(true)}
className="btn-primary flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Создать сервер
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{servers.map((server) => (
<div
key={server.name}
onClick={() => {
setSelectedServer(server);
setCurrentView('server');
}}
className="server-card"
>
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`p-3 rounded-lg ${
server.status === 'running'
? 'bg-green-600/20 text-green-400'
: 'bg-gray-600/20 text-gray-400'
}`}>
<Server className="w-6 h-6" />
</div>
<div>
<h3 className="font-semibold text-lg">{server.displayName}</h3>
<p className="text-sm text-gray-500">{server.name}</p>
</div>
</div>
<span className={server.status === 'running' ? 'badge-success' : 'badge-danger'}>
{server.status === 'running' ? 'Запущен' : 'Остановлен'}
</span>
</div>
<div className="flex items-center gap-2">
{server.status === 'stopped' ? (
<button
onClick={(e) => {
e.stopPropagation();
handleServerAction(server.name, 'start');
}}
className="btn-success flex-1 flex items-center justify-center gap-2"
>
<Play className="w-4 h-4" />
Запустить
</button>
) : (
<button
onClick={(e) => {
e.stopPropagation();
handleServerAction(server.name, 'stop');
}}
className="btn-danger flex-1 flex items-center justify-center gap-2"
>
<Square className="w-4 h-4" />
Остановить
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
setSelectedServer(server);
setCurrentView('server');
}}
className="btn-secondary"
>
<Terminal className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
{servers.length === 0 && (
<div className="card p-12 text-center">
<Server className="w-16 h-16 mx-auto mb-4 text-gray-600" />
<h3 className="text-xl font-semibold mb-2">Нет серверов</h3>
<p className="text-gray-500 mb-4">Создайте свой первый сервер</p>
<button
onClick={() => setShowCreateModal(true)}
className="btn-primary"
>
<Plus className="w-4 h-4 inline mr-2" />
Создать сервер
</button>
</div>
)}
</div>
</div>
)}
{currentView === 'server' && selectedServer && (
<div className="space-y-4 animate-fade-in">
{/* Tabs */}
<div className="card p-2 flex gap-2 overflow-x-auto">
<button
onClick={() => setActiveTab('console')}
className={activeTab === 'console' ? 'tab-active' : 'tab'}
>
<Terminal className="w-4 h-4 inline mr-2" />
Консоль
</button>
<button
onClick={() => setActiveTab('files')}
className={activeTab === 'files' ? 'tab-active' : 'tab'}
>
<FolderOpen className="w-4 h-4 inline mr-2" />
Файлы
</button>
<button
onClick={() => setActiveTab('stats')}
className={activeTab === 'stats' ? 'tab-active' : 'tab'}
>
<BarChart3 className="w-4 h-4 inline mr-2" />
Статистика
</button>
<button
onClick={() => setActiveTab('settings')}
className={activeTab === 'settings' ? 'tab-active' : 'tab'}
>
<Settings className="w-4 h-4 inline mr-2" />
Настройки
</button>
</div>
{/* Tab content */}
<div className="card p-6">
{activeTab === 'console' && <Console serverName={selectedServer.name} token={token} />}
{activeTab === 'files' && <FileManager serverName={selectedServer.name} token={token} />}
{activeTab === 'stats' && <Stats serverName={selectedServer.name} token={token} />}
{activeTab === 'settings' && (
<ServerSettings
serverName={selectedServer.name}
token={token}
onUpdate={loadServers}
/>
)}
</div>
</div>
)}
</div>
</main>
{/* Modals */}
{showCreateModal && (
<CreateServerModal
token={token}
onClose={() => setShowCreateModal(false)}
onSuccess={() => {
setShowCreateModal(false);
loadServers();
}}
/>
)}
{showUserManagement && (
<div className="modal-overlay" onClick={() => setShowUserManagement(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<UserManagement token={token} currentUser={user} />
</div>
</div>
)}
{showDaemons && (
<div className="modal-overlay" onClick={() => setShowDaemons(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<Daemons token={token} />
</div>
</div>
)}
{showTickets && (
<div className="modal-overlay" onClick={() => setShowTickets(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<Tickets token={token} user={user} onClose={() => setShowTickets(false)} />
</div>
</div>
)}
{showProfile && (
<div className="modal-overlay" onClick={() => setShowProfile(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<Profile token={token} user={user} onClose={() => setShowProfile(false)} />
</div>
</div>
)}
<NotificationSystem />
</div>
</ErrorBoundary>
);
}
export default App;