feat: Integrate react-router-dom for routing and enhance App component with new navigation structure
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 13m58s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 13m58s
This commit is contained in:
@@ -16,6 +16,9 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import '/node_modules/@tabler/core/dist/js/tabler.min.js';
|
||||||
|
</script>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Generated
+43
-1
@@ -14,7 +14,8 @@
|
|||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-bootstrap": "^2.10.10",
|
"react-bootstrap": "^2.10.10",
|
||||||
"react-dom": "^19.1.0"
|
"react-dom": "^19.1.0",
|
||||||
|
"react-router-dom": "^6.30.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.29.0",
|
"@eslint/js": "^9.29.0",
|
||||||
@@ -563,6 +564,15 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@remix-run/router": {
|
||||||
|
"version": "1.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
|
||||||
|
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@restart/hooks": {
|
"node_modules/@restart/hooks": {
|
||||||
"version": "0.4.16",
|
"version": "0.4.16",
|
||||||
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
|
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
|
||||||
@@ -2067,6 +2077,38 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "6.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
|
||||||
|
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@remix-run/router": "1.23.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "6.30.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
|
||||||
|
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@remix-run/router": "1.23.0",
|
||||||
|
"react-router": "6.30.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8",
|
||||||
|
"react-dom": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-transition-group": {
|
"node_modules/react-transition-group": {
|
||||||
"version": "4.4.5",
|
"version": "4.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-bootstrap": "^2.10.10",
|
"react-bootstrap": "^2.10.10",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^6.23.1"
|
"react-router-dom": "^6.30.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.29.0",
|
"@eslint/js": "^9.29.0",
|
||||||
|
|||||||
+67
-113
@@ -22,140 +22,94 @@ import DataManager from './DataManager';
|
|||||||
import ServerManager from './ServerManager';
|
import ServerManager from './ServerManager';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import {
|
||||||
|
BrowserRouter as Router,
|
||||||
|
Routes,
|
||||||
|
Route,
|
||||||
|
Link,
|
||||||
|
useLocation,
|
||||||
|
Navigate
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [activeTab, setActiveTab] = useState('dashboard');
|
return (
|
||||||
|
<Router>
|
||||||
|
<MainLayout />
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MainLayout() {
|
||||||
|
const location = useLocation();
|
||||||
const [notifications] = useState([
|
const [notifications] = useState([
|
||||||
{ id: 1, title: 'Новый домен добавлен', message: 'example.com был добавлен в список', time: '2 мин назад' },
|
{ id: 1, title: 'Новый домен добавлен', message: 'example.com был добавлен в список', time: '2 мин назад' },
|
||||||
{ id: 2, title: 'Изменения сохранены', message: 'Список AS обновлен в S3', time: '5 мин назад' }
|
{ id: 2, title: 'Изменения сохранены', message: 'Список AS обновлен в S3', time: '5 мин назад' }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ id: 'dashboard', title: 'Dashboard', icon: IconHome },
|
{ id: 'domains', title: 'Домены', icon: IconWorld, path: '/domains' },
|
||||||
{ id: 'domains', title: 'Домены', icon: IconWorld },
|
{ id: 'asns', title: 'AS', icon: IconNetwork, path: '/asns' },
|
||||||
{ id: 'asns', title: 'AS', icon: IconNetwork },
|
{ id: 'servers', title: 'Серверы', icon: IconServer, path: '/servers' },
|
||||||
{ id: 'servers', title: 'Серверы', icon: IconServer },
|
{ id: 'files', title: 'Файлы', icon: IconFileText, path: '/files' },
|
||||||
{ id: 'files', title: 'Файлы', icon: IconFileText },
|
{ id: 'cloud', title: 'Облако', icon: IconCloud, path: '/cloud' },
|
||||||
{ id: 'cloud', title: 'Облако', icon: IconCloud },
|
{ id: 'security', title: 'Безопасность', icon: IconShield, path: '/security' },
|
||||||
{ id: 'security', title: 'Безопасность', icon: IconShield },
|
{ id: 'activity', title: 'Активность', icon: IconActivity, path: '/activity' },
|
||||||
{ id: 'activity', title: 'Активность', icon: IconActivity },
|
{ id: 'settings', title: 'Настройки', icon: IconSettings, path: '/settings' }
|
||||||
{ id: 'settings', title: 'Настройки', icon: IconSettings }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderContent = () => {
|
// Определяем активную вкладку по адресу
|
||||||
switch (activeTab) {
|
const activeTab = navItems.find(item => location.pathname.startsWith(item.path))?.id || 'domains';
|
||||||
case 'dashboard':
|
|
||||||
return <Dashboard />;
|
|
||||||
case 'domains':
|
|
||||||
return (
|
|
||||||
<DataManager
|
|
||||||
entityName="домен"
|
|
||||||
entityKey="domains"
|
|
||||||
placeholder="example.com"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'asns':
|
|
||||||
return (
|
|
||||||
<DataManager
|
|
||||||
entityName="AS"
|
|
||||||
entityKey="asns"
|
|
||||||
placeholder="AS12345"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'servers':
|
|
||||||
return <ServerManager />;
|
|
||||||
default:
|
|
||||||
return <Dashboard />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
{/* Верхнее меню Tabler Admin */}
|
{/* Верхнее меню Tabler Admin */}
|
||||||
<header className="navbar navbar-expand-md navbar-light d-print-none" style={{boxShadow: '0 1px 0 0 #e9ecef'}}>
|
<header className="navbar navbar-expand-md navbar-light d-print-none" style={{boxShadow: '0 1px 0 0 #e9ecef'}}>
|
||||||
<div className="container-xl">
|
<div className="container-xl">
|
||||||
<a href="#" className="navbar-brand d-flex align-items-center">
|
<Link to="/" className="navbar-brand d-flex align-items-center">
|
||||||
<IconBrandTabler className="navbar-brand-icon me-2" />
|
<IconBrandTabler className="navbar-brand-icon me-2" />
|
||||||
<span className="fw-bold">S3 Lists Manager</span>
|
<span className="fw-bold">S3 Lists Manager</span>
|
||||||
</a>
|
</Link>
|
||||||
<div className="collapse navbar-collapse show" id="navbar-menu">
|
|
||||||
<ul className="navbar-nav me-auto">
|
|
||||||
{navItems.map((item) => (
|
|
||||||
<li key={item.id} className="nav-item">
|
|
||||||
<a
|
|
||||||
className={`nav-link d-flex align-items-center ${activeTab === item.id ? 'active fw-bold' : ''}`}
|
|
||||||
href="#"
|
|
||||||
onClick={e => { e.preventDefault(); setActiveTab(item.id); }}
|
|
||||||
>
|
|
||||||
<item.icon className="nav-link-icon me-1" size={20} />
|
|
||||||
{item.title}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<ul className="navbar-nav ms-auto">
|
|
||||||
<li className="nav-item dropdown">
|
|
||||||
<a href="#" className="nav-link px-0" data-bs-toggle="dropdown" tabIndex="-1">
|
|
||||||
<IconBell className="icon" />
|
|
||||||
<span className="badge badge-sm bg-red"></span>
|
|
||||||
</a>
|
|
||||||
<div className="dropdown-menu dropdown-menu-arrow dropdown-menu-end dropdown-menu-card">
|
|
||||||
<div className="card">
|
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="card-title">Уведомления</h3>
|
|
||||||
</div>
|
|
||||||
<div className="list-group list-group-flush">
|
|
||||||
{notifications.map((notification) => (
|
|
||||||
<div key={notification.id} className="list-group-item">
|
|
||||||
<div className="row align-items-center">
|
|
||||||
<div className="col-auto">
|
|
||||||
<span className="status-dot status-dot-animated bg-red d-block"></span>
|
|
||||||
</div>
|
|
||||||
<div className="col text-truncate">
|
|
||||||
<a href="#" className="text-body d-block">{notification.title}</a>
|
|
||||||
<div className="d-block text-muted text-truncate mt-n1">
|
|
||||||
{notification.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li className="nav-item dropdown ms-3">
|
|
||||||
<a href="#" className="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown">
|
|
||||||
<span className="avatar avatar-sm" style={{backgroundImage: 'url(./static/avatars/000m.jpg)'}}></span>
|
|
||||||
<span className="d-none d-xl-block ps-2">
|
|
||||||
<span>Admin User</span>
|
|
||||||
<IconChevronDown className="icon" />
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
|
||||||
<a href="#" className="dropdown-item">
|
|
||||||
<IconUser className="icon me-2" />
|
|
||||||
Профиль
|
|
||||||
</a>
|
|
||||||
<a href="#" className="dropdown-item">
|
|
||||||
<IconSettings className="icon me-2" />
|
|
||||||
Настройки
|
|
||||||
</a>
|
|
||||||
<div className="dropdown-divider"></div>
|
|
||||||
<a href="#" className="dropdown-item">
|
|
||||||
Выйти
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="page-wrapper">
|
<div className="container-xl mt-4">
|
||||||
<div className="container-xl mt-4">
|
<div className="row">
|
||||||
{renderContent()}
|
<div className="col-2">
|
||||||
|
<nav className="nav flex-column nav-pills">
|
||||||
|
{navItems.slice(0, 3).map(item => (
|
||||||
|
<Link
|
||||||
|
key={item.id}
|
||||||
|
to={item.path}
|
||||||
|
className={`nav-link${activeTab === item.id ? ' active' : ''}`}
|
||||||
|
style={{display: 'flex', alignItems: 'center'}}>
|
||||||
|
<item.icon className="me-2" />
|
||||||
|
{item.title}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div className="col-10">
|
||||||
|
<Routes>
|
||||||
|
<Route path="/domains" element={
|
||||||
|
<DataManager
|
||||||
|
entityName="домен"
|
||||||
|
entityKey="domains"
|
||||||
|
placeholder="example.com"
|
||||||
|
/>
|
||||||
|
} />
|
||||||
|
<Route path="/asns" element={
|
||||||
|
<DataManager
|
||||||
|
entityName="AS"
|
||||||
|
entityKey="asns"
|
||||||
|
placeholder="AS12345"
|
||||||
|
/>
|
||||||
|
} />
|
||||||
|
<Route path="/servers" element={<ServerManager />} />
|
||||||
|
<Route path="/" element={<Navigate to="/domains" replace />} />
|
||||||
|
{/* Можно добавить другие разделы по необходимости */}
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -526,17 +526,35 @@ function ServerManager() {
|
|||||||
|
|
||||||
// Модальное окно для редактирования сервера с backdrop и анимацией
|
// Модальное окно для редактирования сервера с backdrop и анимацией
|
||||||
function EditServerModal({ show, server, onChange, onSave, onClose }) {
|
function EditServerModal({ show, server, onChange, onSave, onClose }) {
|
||||||
if (!show || !server) return null;
|
const modalRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.Tabler && window.Tabler.Modal && modalRef.current) {
|
||||||
|
const modalInstance = window.Tabler.Modal.getOrCreateInstance(modalRef.current);
|
||||||
|
if (show) {
|
||||||
|
modalInstance.show();
|
||||||
|
} else {
|
||||||
|
modalInstance.hide();
|
||||||
|
}
|
||||||
|
// Закрытие по событию Tabler
|
||||||
|
const handler = () => onClose && onClose();
|
||||||
|
modalRef.current.addEventListener('hide.bs.modal', handler);
|
||||||
|
return () => {
|
||||||
|
modalRef.current.removeEventListener('hide.bs.modal', handler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [show, onClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="modal" tabIndex="-1" ref={modalRef}>
|
||||||
<div className="modal fade show" style={{display: 'block'}} tabIndex="-1" role="dialog" aria-modal="true">
|
<div className="modal-dialog">
|
||||||
<div className="modal-dialog" role="document">
|
<div className="modal-content">
|
||||||
<div className="modal-content animate__animated animate__fadeInDown">
|
<div className="modal-header">
|
||||||
<div className="modal-header">
|
<h5 className="modal-title">Редактировать сервер</h5>
|
||||||
<h5 className="modal-title">Редактировать сервер</h5>
|
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
<button type="button" className="btn-close" onClick={onClose}></button>
|
</div>
|
||||||
</div>
|
<div className="modal-body">
|
||||||
<div className="modal-body">
|
{server && (
|
||||||
<form onSubmit={e => { e.preventDefault(); onSave(); }}>
|
<form onSubmit={e => { e.preventDefault(); onSave(); }}>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="form-label">IP адрес</label>
|
<label className="form-label">IP адрес</label>
|
||||||
@@ -559,16 +577,15 @@ function EditServerModal({ show, server, onChange, onSave, onClose }) {
|
|||||||
<input type="text" className="form-control" value={server.tunnel} onChange={e => onChange({ ...server, tunnel: e.target.value })} />
|
<input type="text" className="form-control" value={server.tunnel} onChange={e => onChange({ ...server, tunnel: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
)}
|
||||||
<div className="modal-footer">
|
</div>
|
||||||
<button type="button" className="btn btn-secondary" onClick={onClose}>Отмена</button>
|
<div className="modal-footer">
|
||||||
<button type="button" className="btn btn-primary" onClick={onSave}>Сохранить</button>
|
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
</div>
|
<button type="button" className="btn btn-primary" onClick={onSave}>Сохранить</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-backdrop fade show"></div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user