feat: Integrate @tabler/core for UI enhancements and refactor App component with new sidebar and dashboard features
Publish Tabler Docker image / build-and-push-tabler (push) Has been cancelled
Publish Tabler Docker image / build-and-push-tabler (push) Has been cancelled
This commit is contained in:
Generated
+37
@@ -8,6 +8,7 @@
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@tabler/core": "^1.3.2",
|
||||
"@tabler/icons-react": "^3.34.0",
|
||||
"axios": "^1.10.0",
|
||||
"bootstrap": "^5.3.7",
|
||||
@@ -642,6 +643,42 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tabler/core": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/core/-/core-1.3.2.tgz",
|
||||
"integrity": "sha512-QDVJbv48YJrahBLdxYkLi6NutQv4jGbkUWyzxE2NcNJ3s3GGpRx98JmbAoN92NZKNmf26vZdW6k2Q5haVKlS4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"bootstrap": "5.3.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/codecalm"
|
||||
}
|
||||
},
|
||||
"node_modules/@tabler/core/node_modules/bootstrap": {
|
||||
"version": "5.3.6",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
|
||||
"integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/twbs"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.11.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tabler/icons": {
|
||||
"version": "3.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.34.0.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/core": "^1.3.2",
|
||||
"@tabler/icons-react": "^3.34.0",
|
||||
"axios": "^1.10.0",
|
||||
"bootstrap": "^5.3.7",
|
||||
|
||||
+34
-931
File diff suppressed because it is too large
Load Diff
+284
-60
@@ -6,36 +6,96 @@ import {
|
||||
IconNetwork,
|
||||
IconSettings,
|
||||
IconMenu2,
|
||||
IconX
|
||||
IconX,
|
||||
IconHome,
|
||||
IconDatabase,
|
||||
IconUsers,
|
||||
IconBell,
|
||||
IconSearch,
|
||||
IconChevronDown,
|
||||
IconLogout,
|
||||
IconUser,
|
||||
IconDashboard,
|
||||
IconFileText,
|
||||
IconCloud,
|
||||
IconShield,
|
||||
IconActivity
|
||||
} from '@tabler/icons-react';
|
||||
import DataManager from './DataManager';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('domains');
|
||||
const [activeTab, setActiveTab] = useState('dashboard');
|
||||
const [notifications] = useState([
|
||||
{ id: 1, title: 'Новый домен добавлен', message: 'example.com был добавлен в список', time: '2 мин назад' },
|
||||
{ id: 2, title: 'Изменения сохранены', message: 'Список AS обновлен в S3', time: '5 мин назад' }
|
||||
]);
|
||||
|
||||
const toggleSidebar = () => setSidebarOpen(!sidebarOpen);
|
||||
|
||||
const sidebarItems = [
|
||||
{ id: 'dashboard', title: 'Dashboard', icon: IconDashboard, badge: null },
|
||||
{ id: 'domains', title: 'Домены', icon: IconWorld, badge: '12' },
|
||||
{ id: 'asns', title: 'AS', icon: IconNetwork, badge: '8' },
|
||||
{ id: 'files', title: 'Файлы', icon: IconFileText, badge: null },
|
||||
{ id: 'cloud', title: 'Облако', icon: IconCloud, badge: null },
|
||||
{ id: 'security', title: 'Безопасность', icon: IconShield, badge: null },
|
||||
{ id: 'activity', title: 'Активность', icon: IconActivity, badge: '3' },
|
||||
{ id: 'settings', title: 'Настройки', icon: IconSettings, badge: null }
|
||||
];
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'dashboard':
|
||||
return <Dashboard />;
|
||||
case 'domains':
|
||||
return (
|
||||
<DataManager
|
||||
entityName="домен"
|
||||
entityKey="domains"
|
||||
placeholder="example.com"
|
||||
/>
|
||||
);
|
||||
case 'asns':
|
||||
return (
|
||||
<DataManager
|
||||
entityName="AS"
|
||||
entityKey="asns"
|
||||
placeholder="AS12345"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <Dashboard />;
|
||||
}
|
||||
};
|
||||
|
||||
const getPageTitle = () => {
|
||||
const item = sidebarItems.find(item => item.id === activeTab);
|
||||
return item ? item.title : 'Dashboard';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
{/* Header */}
|
||||
<header className="navbar navbar-expand-md navbar-light d-print-none">
|
||||
<div className="container-xl">
|
||||
{/* Sidebar */}
|
||||
<aside className={`navbar navbar-vertical navbar-expand-lg ${sidebarOpen ? 'navbar-show' : ''}`}>
|
||||
<div className="container-fluid">
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<IconMenu2 className="icon" />
|
||||
<IconX className="icon" />
|
||||
</button>
|
||||
<h1 className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||
|
||||
<h1 className="navbar-brand navbar-brand-autodark">
|
||||
<IconBrandTabler className="navbar-brand-icon me-2" />
|
||||
S3 Lists Manager
|
||||
</h1>
|
||||
<div className="navbar-nav flex-row order-md-last">
|
||||
|
||||
<div className="navbar-nav flex-row d-lg-none">
|
||||
<div className="nav-item dropdown">
|
||||
<a href="#" className="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
|
||||
<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>
|
||||
</a>
|
||||
<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||
@@ -46,83 +106,164 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className={`navbar-expand-md ${sidebarOpen ? 'show' : ''}`}>
|
||||
<div className="collapse navbar-collapse" id="navbar-menu">
|
||||
<div className="navbar navbar-light">
|
||||
<div className="container-xl">
|
||||
<ul className="navbar-nav">
|
||||
<li className="nav-item">
|
||||
<div className="collapse navbar-collapse" id="sidebar-menu">
|
||||
<ul className="navbar-nav pt-lg-auto">
|
||||
{sidebarItems.map((item) => (
|
||||
<li key={item.id} className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${activeTab === 'domains' ? 'active' : ''}`}
|
||||
className={`nav-link ${activeTab === item.id ? 'active' : ''}`}
|
||||
href="#"
|
||||
onClick={(e) => { e.preventDefault(); setActiveTab('domains'); }}
|
||||
onClick={(e) => { e.preventDefault(); setActiveTab(item.id); }}
|
||||
>
|
||||
<IconWorld className="nav-link-icon" />
|
||||
<span className="nav-link-title">Домены</span>
|
||||
<item.icon className="nav-link-icon" />
|
||||
<span className="nav-link-title">{item.title}</span>
|
||||
{item.badge && (
|
||||
<span className="badge badge-sm bg-green-lt text-green ms-auto">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${activeTab === 'asns' ? 'active' : ''}`}
|
||||
href="#"
|
||||
onClick={(e) => { e.preventDefault(); setActiveTab('asns'); }}
|
||||
>
|
||||
<IconNetwork className="nav-link-icon" />
|
||||
<span className="nav-link-title">AS</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" href="#">
|
||||
<IconSettings className="nav-link-icon" />
|
||||
<span className="nav-link-title">Настройки</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Header */}
|
||||
<header className="navbar navbar-expand-md navbar-light d-none d-lg-flex d-print-none">
|
||||
<div className="container-xl">
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<IconMenu2 className="icon" />
|
||||
</button>
|
||||
|
||||
<div className="navbar-nav flex-row order-md-last">
|
||||
<div className="nav-item dropdown d-none d-md-flex me-3">
|
||||
<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 className="col-auto">
|
||||
<a href="#" className="list-group-item-actions">
|
||||
<IconX className="icon" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nav-item dropdown">
|
||||
<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" />
|
||||
Profile
|
||||
</a>
|
||||
<a href="#" className="dropdown-item">
|
||||
<IconSettings className="icon me-2" />
|
||||
Settings
|
||||
</a>
|
||||
<div className="dropdown-divider"></div>
|
||||
<a href="#" className="dropdown-item">
|
||||
<IconLogout className="icon me-2" />
|
||||
Logout
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="navbar-nav flex-row">
|
||||
<div className="nav-item d-none d-md-flex me-3">
|
||||
<div className="input-icon">
|
||||
<IconSearch className="icon" />
|
||||
<input type="text" className="form-control" placeholder="Поиск..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="page-wrapper">
|
||||
<div className="container-xl">
|
||||
{/* Page Header */}
|
||||
<div className="page-header d-print-none">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<div className="page-pretitle">
|
||||
S3 Lists Manager
|
||||
</div>
|
||||
<h2 className="page-title">
|
||||
{activeTab === 'domains' ? 'Управление доменами' : 'Управление AS'}
|
||||
{getPageTitle()}
|
||||
</h2>
|
||||
<div className="text-muted mt-1">
|
||||
{activeTab === 'domains'
|
||||
? 'Добавляйте, редактируйте и управляйте списками доменов'
|
||||
: 'Добавляйте, редактируйте и управляйте списками AS'
|
||||
}
|
||||
</div>
|
||||
<div className="col-auto ms-auto d-print-none">
|
||||
<div className="btn-list">
|
||||
<span className="d-none d-sm-inline">
|
||||
<a href="#" className="btn btn-white">
|
||||
Новый домен
|
||||
</a>
|
||||
</span>
|
||||
<a href="#" className="btn btn-primary d-none d-sm-inline-block">
|
||||
<IconDatabase className="icon me-2" />
|
||||
Сохранить в S3
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Breadcrumbs */}
|
||||
<div className="page-header-breadcrumb">
|
||||
<ol className="breadcrumb">
|
||||
<li className="breadcrumb-item">
|
||||
<a href="#" onClick={(e) => { e.preventDefault(); setActiveTab('dashboard'); }}>
|
||||
<IconHome className="icon me-2" />
|
||||
Главная
|
||||
</a>
|
||||
</li>
|
||||
<li className="breadcrumb-item active" aria-current="page">
|
||||
{getPageTitle()}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-body">
|
||||
<div className="container-xl">
|
||||
{activeTab === 'domains' && (
|
||||
<DataManager
|
||||
entityName="домен"
|
||||
entityKey="domains"
|
||||
placeholder="example.com"
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'asns' && (
|
||||
<DataManager
|
||||
entityName="AS"
|
||||
entityKey="asns"
|
||||
placeholder="AS12345"
|
||||
/>
|
||||
)}
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -147,4 +288,87 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
// Dashboard Component
|
||||
function Dashboard() {
|
||||
const stats = [
|
||||
{ title: 'Всего доменов', value: '1,234', icon: IconWorld, color: 'blue' },
|
||||
{ title: 'Всего AS', value: '567', icon: IconNetwork, color: 'green' },
|
||||
{ title: 'Активные шлюзы', value: '12', icon: IconDatabase, color: 'orange' },
|
||||
{ title: 'Последние изменения', value: '3', icon: IconActivity, color: 'red' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="row row-deck row-cards">
|
||||
{/* Stats Cards */}
|
||||
{stats.map((stat, index) => (
|
||||
<div key={index} className="col-sm-6 col-lg-3">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="subheader">{stat.title}</div>
|
||||
</div>
|
||||
<div className="h1 mb-3">{stat.value}</div>
|
||||
<div className="d-flex mb-2">
|
||||
<stat.icon className={`icon icon-sm text-${stat.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Main Content Card */}
|
||||
<div className="col-12">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">Добро пожаловать в S3 Lists Manager</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p>Выберите раздел в боковом меню для управления списками доменов и AS.</p>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<div className="card card-sm">
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<IconWorld className="icon icon-lg text-blue" />
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="font-weight-medium">
|
||||
Управление доменами
|
||||
</div>
|
||||
<div className="text-muted">
|
||||
Добавляйте, редактируйте и управляйте списками доменов
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="card card-sm">
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<IconNetwork className="icon icon-lg text-green" />
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="font-weight-medium">
|
||||
Управление AS
|
||||
</div>
|
||||
<div className="text-muted">
|
||||
Добавляйте, редактируйте и управляйте списками AS
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
+109
-169
@@ -1,6 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Table, Button, Form, Row, Col, Card, Alert } from 'react-bootstrap';
|
||||
import {
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
@@ -199,7 +198,7 @@ function DataManager({ entityName, entityKey, placeholder }) {
|
||||
</div>
|
||||
|
||||
{/* Bulk Update Card */}
|
||||
<div className="card card-md mt-3">
|
||||
<div className="card card-md">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
<IconArrowsExchange className="icon me-2" />
|
||||
@@ -207,110 +206,100 @@ function DataManager({ entityName, entityKey, placeholder }) {
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="mb-2">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Заменить с</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={bulkFrom}
|
||||
onChange={e => setBulkFrom(e.target.value)}
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={bulkFrom}
|
||||
onChange={(e) => setBulkFrom(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Заменить на</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={bulkTo}
|
||||
onChange={e => setBulkTo(e.target.value)}
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={bulkTo}
|
||||
onChange={(e) => setBulkTo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-warning w-100"
|
||||
className="btn btn-warning w-100"
|
||||
onClick={handleBulkUpdate}
|
||||
>
|
||||
<IconArrowsExchange className="icon me-2" />
|
||||
Выполнить замену
|
||||
Обновить все
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Card */}
|
||||
<div className="card card-md mt-3">
|
||||
{/* Actions Card */}
|
||||
<div className="card card-md">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
<IconDatabase className="icon me-2" />
|
||||
Статистика
|
||||
Действия
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="row text-center">
|
||||
<div className="col">
|
||||
<div className="text-muted mb-1">Всего</div>
|
||||
<div className="h3 mb-0">{items.length}</div>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="text-muted mb-1">Найдено</div>
|
||||
<div className="h3 mb-0">{filteredItems.length}</div>
|
||||
</div>
|
||||
<div className="d-grid gap-2">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleSaveChanges}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="spinner-border spinner-border-sm me-2" role="status"></span>
|
||||
Сохранение...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconDatabase className="icon me-2" />
|
||||
Сохранить в S3
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-secondary"
|
||||
onClick={fetchItems}
|
||||
disabled={loading}
|
||||
>
|
||||
<IconRefresh className="icon me-2" />
|
||||
Обновить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-lg-8">
|
||||
{/* Main Data Card */}
|
||||
{/* Search and Table Card */}
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<h3 className="card-title">
|
||||
<IconDatabase className="icon me-2" />
|
||||
Список: {entityName}
|
||||
</h3>
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className="btn btn-outline-secondary me-2"
|
||||
onClick={fetchItems}
|
||||
disabled={loading}
|
||||
>
|
||||
<IconRefresh className="icon me-2" />
|
||||
Обновить
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-success"
|
||||
onClick={handleSaveChanges}
|
||||
disabled={items.length === 0 || loading}
|
||||
>
|
||||
<IconUpload className="icon me-2" />
|
||||
Сохранить в S3
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{/* Search */}
|
||||
<div className="mb-3">
|
||||
<h3 className="card-title">Список {entityName}</h3>
|
||||
<div className="card-actions">
|
||||
<div className="input-icon">
|
||||
<IconSearch className="icon" />
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder={`Поиск по ${entityName}...`}
|
||||
placeholder={`Поиск ${entityName}...`}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="table-responsive">
|
||||
<table className="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>{entityName}</th>
|
||||
<th>Шлюз</th>
|
||||
<th className="w-1"></th>
|
||||
@@ -318,10 +307,18 @@ function DataManager({ entityName, entityKey, placeholder }) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredItems.map((item, index) => (
|
||||
<tr key={item.domain}>
|
||||
<td className="text-muted">{index + 1}</td>
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<div className="font-weight-medium">{item.domain}</div>
|
||||
{editingDomain === item.domain ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
value={editingDomain}
|
||||
onChange={(e) => setEditingDomain(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
item.domain
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{editingDomain === item.domain ? (
|
||||
@@ -330,7 +327,6 @@ function DataManager({ entityName, entityKey, placeholder }) {
|
||||
className="form-control form-control-sm"
|
||||
value={editingValue}
|
||||
onChange={(e) => setEditingValue(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<span className="badge bg-blue-lt">{item.type}</span>
|
||||
@@ -339,14 +335,14 @@ function DataManager({ entityName, entityKey, placeholder }) {
|
||||
<td>
|
||||
{editingDomain === item.domain ? (
|
||||
<div className="btn-list">
|
||||
<button
|
||||
className="btn btn-sm btn-success"
|
||||
<button
|
||||
className="btn btn-sm btn-success"
|
||||
onClick={() => handleSaveEdit(item.domain)}
|
||||
>
|
||||
<IconCheck className="icon" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-secondary"
|
||||
<button
|
||||
className="btn btn-sm btn-secondary"
|
||||
onClick={handleCancelEdit}
|
||||
>
|
||||
<IconX className="icon" />
|
||||
@@ -354,14 +350,14 @@ function DataManager({ entityName, entityKey, placeholder }) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="btn-list">
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
onClick={() => handleEdit(item)}
|
||||
>
|
||||
<IconEdit className="icon" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-danger"
|
||||
<button
|
||||
className="btn btn-sm btn-outline-danger"
|
||||
onClick={() => confirmDelete(item)}
|
||||
>
|
||||
<IconTrash className="icon" />
|
||||
@@ -374,39 +370,17 @@ function DataManager({ entityName, entityKey, placeholder }) {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Empty States */}
|
||||
{items.length > 0 && filteredItems.length === 0 && (
|
||||
{filteredItems.length === 0 && (
|
||||
<div className="empty">
|
||||
<div className="empty-icon">
|
||||
<IconSearch className="icon" />
|
||||
<div className="empty-img">
|
||||
<IconAlertTriangle className="icon" />
|
||||
</div>
|
||||
<p className="empty-title">Ничего не найдено</p>
|
||||
<p className="empty-title">Нет {entityName} для отображения</p>
|
||||
<p className="empty-subtitle text-muted">
|
||||
По вашему запросу ничего не найдено.
|
||||
Добавьте первый {entityName} используя форму слева.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{items.length === 0 && !loading && (
|
||||
<div className="empty">
|
||||
<div className="empty-icon">
|
||||
<IconDatabase className="icon" />
|
||||
</div>
|
||||
<p className="empty-title">Список пуст</p>
|
||||
<p className="empty-subtitle text-muted">
|
||||
Добавьте новый элемент, чтобы начать работу.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="text-center py-4">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -414,77 +388,43 @@ function DataManager({ entityName, entityKey, placeholder }) {
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{showDeleteModal && (
|
||||
<div className="modal-backdrop" style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 1050,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<div className="modal-dialog" style={{
|
||||
maxWidth: '500px',
|
||||
width: '90%',
|
||||
margin: '1.75rem auto'
|
||||
}}>
|
||||
<div className="modal-content" style={{
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '0.5rem',
|
||||
boxShadow: '0 0.5rem 1rem rgba(0, 0, 0, 0.15)'
|
||||
}}>
|
||||
<div className="modal-header" style={{
|
||||
padding: '1rem 1.5rem',
|
||||
borderBottom: '1px solid var(--tblr-border-color)',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<IconAlertTriangle className="icon me-2" style={{ color: 'var(--tblr-danger)' }} />
|
||||
<h5 className="modal-title" style={{ margin: 0 }}>Подтверждение удаления</h5>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
onClick={() => setShowDeleteModal(false)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '1.5rem',
|
||||
cursor: 'pointer',
|
||||
marginLeft: 'auto'
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div className="modal modal-blur fade show" style={{display: 'block'}} tabIndex="-1">
|
||||
<div className="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||
<div className="modal-content">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
onClick={() => setShowDeleteModal(false)}
|
||||
></button>
|
||||
<div className="modal-status bg-danger"></div>
|
||||
<div className="modal-body text-center py-4">
|
||||
<IconTrash className="icon mb-2 text-danger icon-lg" />
|
||||
<h3>Удалить {entityName}?</h3>
|
||||
<div className="text-muted">
|
||||
Вы уверены, что хотите удалить "{itemToDelete?.domain}"? Это действие необратимо.
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-body" style={{ padding: '1.5rem' }}>
|
||||
<p>Вы уверены, что хотите удалить <strong>{itemToDelete?.domain}</strong>?</p>
|
||||
<p className="text-muted mb-0">Это действие нельзя отменить.</p>
|
||||
</div>
|
||||
<div className="modal-footer" style={{
|
||||
padding: '1rem 1.5rem',
|
||||
borderTop: '1px solid var(--tblr-border-color)',
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
justifyContent: 'flex-end'
|
||||
}}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setShowDeleteModal(false)}
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger"
|
||||
onClick={executeDelete}
|
||||
>
|
||||
<IconTrash className="icon me-2" />
|
||||
Удалить
|
||||
</button>
|
||||
<div className="modal-footer">
|
||||
<div className="w-100">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<button
|
||||
className="btn w-100"
|
||||
onClick={() => setShowDeleteModal(false)}
|
||||
>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
<div className="col">
|
||||
<button
|
||||
className="btn btn-danger w-100"
|
||||
onClick={executeDelete}
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
import '@tabler/core/dist/css/tabler.min.css'
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
|
||||
Reference in New Issue
Block a user