feat: Introduce cursor utility class for pointer interactions and refactor action buttons into PageHeaderActions component across multiple managers for improved UI consistency
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m54s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m54s
This commit is contained in:
@@ -15,11 +15,13 @@ import {
|
|||||||
IconDeviceFloppy,
|
IconDeviceFloppy,
|
||||||
IconHash,
|
IconHash,
|
||||||
IconClock,
|
IconClock,
|
||||||
IconFileText
|
IconFileText,
|
||||||
|
IconEye
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import LockBanner from './components/LockBanner.jsx';
|
import LockBanner from './components/LockBanner.jsx';
|
||||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
import TableSkeleton from './components/TableSkeleton.jsx';
|
||||||
import S3MetaBar from './components/S3MetaBar.jsx';
|
import S3MetaBar from './components/S3MetaBar.jsx';
|
||||||
|
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
||||||
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
||||||
import HistoryModal from './components/HistoryModal.jsx';
|
import HistoryModal from './components/HistoryModal.jsx';
|
||||||
|
|
||||||
@@ -393,9 +395,21 @@ function ASNsNewManager() {
|
|||||||
<div className="page-pretitle">Главная / Данные / ASNs</div>
|
<div className="page-pretitle">Главная / Данные / ASNs</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-auto ms-auto d-print-none">
|
<div className="col-auto ms-auto d-print-none">
|
||||||
<div className="btn-list">
|
<PageHeaderActions
|
||||||
<button className="btn btn-outline-secondary" onClick={() => setHistoryOpen(true)}>История</button>
|
loading={loading}
|
||||||
</div>
|
onPreview={handlePreviewDiff}
|
||||||
|
disablePreview={loading || items.length === 0}
|
||||||
|
onRefresh={fetchItems}
|
||||||
|
disableRefresh={loading}
|
||||||
|
onImport={handleImport}
|
||||||
|
onExport={handleExport}
|
||||||
|
disableExport={items.length === 0}
|
||||||
|
onClear={clearInvalid}
|
||||||
|
disableClear={items.length === 0}
|
||||||
|
onSave={handleSaveChanges}
|
||||||
|
disableSave={loading}
|
||||||
|
onHistory={() => setHistoryOpen(true)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -467,73 +481,7 @@ function ASNsNewManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions Card */}
|
{/* Карточка действий больше не нужна — действия перенесены в page-header */}
|
||||||
<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="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>
|
|
||||||
Сохранение...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<IconDeviceFloppy className="icon me-2" />
|
|
||||||
Сохранить в S3
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={fetchItems}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<IconRefresh className="icon me-2" />
|
|
||||||
Обновить
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-primary"
|
|
||||||
onClick={handleImport}
|
|
||||||
>
|
|
||||||
<IconUpload className="icon me-2" />
|
|
||||||
Импорт из буфера
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-primary"
|
|
||||||
onClick={handleExport}
|
|
||||||
disabled={items.length === 0}
|
|
||||||
>
|
|
||||||
<IconDownload className="icon me-2" />
|
|
||||||
Экспорт CSV
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={handlePreviewDiff}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
Предпросмотр изменений
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={clearInvalid}
|
|
||||||
disabled={items.length === 0}
|
|
||||||
>
|
|
||||||
Очистить пустые/невалидные
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-lg-9">
|
<div className="col-lg-9">
|
||||||
@@ -572,7 +520,7 @@ function ASNsNewManager() {
|
|||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('asn')}>
|
<th className="cursor-pointer" onClick={() => handleSort('asn')}>
|
||||||
Номер AS
|
Номер AS
|
||||||
{sortField === 'asn' && (
|
{sortField === 'asn' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
@@ -580,7 +528,7 @@ function ASNsNewManager() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('community')}>
|
<th className="cursor-pointer" onClick={() => handleSort('community')}>
|
||||||
Community
|
Community
|
||||||
{sortField === 'community' && (
|
{sortField === 'community' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
|
|||||||
@@ -219,6 +219,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Utility: pointer cursor */
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom responsive adjustments */
|
/* Custom responsive adjustments */
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
.page-wrapper {
|
.page-wrapper {
|
||||||
|
|||||||
@@ -683,64 +683,43 @@ function BillingManager() {
|
|||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th className="cursor-pointer" onClick={() => handleSort('hostName')}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => handleSort('hostName')}
|
|
||||||
>
|
|
||||||
Имя хостера
|
Имя хостера
|
||||||
{sortField === 'hostName' && (
|
{sortField === 'hostName' && (
|
||||||
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="cursor-pointer" onClick={() => handleSort('purpose')}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => handleSort('purpose')}
|
|
||||||
>
|
|
||||||
Назначение
|
Назначение
|
||||||
{sortField === 'purpose' && (
|
{sortField === 'purpose' && (
|
||||||
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="cursor-pointer" onClick={() => handleSort('country')}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => handleSort('country')}
|
|
||||||
>
|
|
||||||
Страна
|
Страна
|
||||||
{sortField === 'country' && (
|
{sortField === 'country' && (
|
||||||
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="cursor-pointer" onClick={() => handleSort('provider')}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => handleSort('provider')}
|
|
||||||
>
|
|
||||||
Провайдер
|
Провайдер
|
||||||
{sortField === 'provider' && (
|
{sortField === 'provider' && (
|
||||||
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="cursor-pointer" onClick={() => handleSort('monthlyCost')}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => handleSort('monthlyCost')}
|
|
||||||
>
|
|
||||||
Месячная стоимость
|
Месячная стоимость
|
||||||
{sortField === 'monthlyCost' && (
|
{sortField === 'monthlyCost' && (
|
||||||
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="cursor-pointer" onClick={() => handleSort('nextPaymentDate')}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => handleSort('nextPaymentDate')}
|
|
||||||
>
|
|
||||||
Следующий платеж
|
Следующий платеж
|
||||||
{sortField === 'nextPaymentDate' && (
|
{sortField === 'nextPaymentDate' && (
|
||||||
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th className="cursor-pointer" onClick={() => handleSort('status')}>
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => handleSort('status')}
|
|
||||||
>
|
|
||||||
Статус
|
Статус
|
||||||
{sortField === 'status' && (
|
{sortField === 'status' && (
|
||||||
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
<span className="ms-1">{sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
|
|||||||
@@ -260,8 +260,8 @@ function CommunitiesManager() {
|
|||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{cursor:'pointer'}} onClick={()=>changeSort('value')}>Value {sortField==='value' && <span className="ms-1">{sortOrder==='asc'?'▲':'▼'}</span>}</th>
|
<th className="cursor-pointer" onClick={()=>changeSort('value')}>Value {sortField==='value' && <span className="ms-1">{sortOrder==='asc'?'▲':'▼'}</span>}</th>
|
||||||
<th style={{cursor:'pointer'}} onClick={()=>changeSort('name')}>Название {sortField==='name' && <span className="ms-1">{sortOrder==='asc'?'▲':'▼'}</span>}</th>
|
<th className="cursor-pointer" onClick={()=>changeSort('name')}>Название {sortField==='name' && <span className="ms-1">{sortOrder==='asc'?'▲':'▼'}</span>}</th>
|
||||||
<th>Описание</th>
|
<th>Описание</th>
|
||||||
<th>Теги</th>
|
<th>Теги</th>
|
||||||
<th>Gateway</th>
|
<th>Gateway</th>
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ function DataManager({ entityName, entityKey, placeholder }) {
|
|||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('domain')}>
|
<th className="cursor-pointer" onClick={() => handleSort('domain')}>
|
||||||
Домен
|
Домен
|
||||||
{sortField === 'domain' && (
|
{sortField === 'domain' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
@@ -372,7 +372,7 @@ function DataManager({ entityName, entityKey, placeholder }) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('type')}>
|
<th className="cursor-pointer" onClick={() => handleSort('type')}>
|
||||||
Шлюз
|
Шлюз
|
||||||
{sortField === 'type' && (
|
{sortField === 'type' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ import {
|
|||||||
IconDeviceFloppy,
|
IconDeviceFloppy,
|
||||||
IconHash,
|
IconHash,
|
||||||
IconClock,
|
IconClock,
|
||||||
IconFileText
|
IconFileText,
|
||||||
|
IconEye
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import LockBanner from './components/LockBanner.jsx';
|
import LockBanner from './components/LockBanner.jsx';
|
||||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
import TableSkeleton from './components/TableSkeleton.jsx';
|
||||||
import S3MetaBar from './components/S3MetaBar.jsx';
|
import S3MetaBar from './components/S3MetaBar.jsx';
|
||||||
|
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
||||||
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
||||||
import HistoryModal from './components/HistoryModal.jsx';
|
import HistoryModal from './components/HistoryModal.jsx';
|
||||||
|
|
||||||
@@ -400,9 +402,21 @@ function DomainsNewManager() {
|
|||||||
<div className="page-pretitle">Главная / Данные / Домены</div>
|
<div className="page-pretitle">Главная / Данные / Домены</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-auto ms-auto d-print-none">
|
<div className="col-auto ms-auto d-print-none">
|
||||||
<div className="btn-list">
|
<PageHeaderActions
|
||||||
<button className="btn btn-outline-secondary" onClick={() => setHistoryOpen(true)}>История</button>
|
loading={loading}
|
||||||
</div>
|
onPreview={handlePreviewDiff}
|
||||||
|
disablePreview={loading || items.length === 0}
|
||||||
|
onRefresh={fetchItems}
|
||||||
|
disableRefresh={loading}
|
||||||
|
onImport={handleImport}
|
||||||
|
onExport={handleExport}
|
||||||
|
disableExport={items.length === 0}
|
||||||
|
onClear={clearInvalid}
|
||||||
|
disableClear={items.length === 0}
|
||||||
|
onSave={handleSaveChanges}
|
||||||
|
disableSave={loading}
|
||||||
|
onHistory={() => setHistoryOpen(true)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -465,73 +479,7 @@ function DomainsNewManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions Card */}
|
{/* Карточка действий больше не нужна — действия перенесены в page-header */}
|
||||||
<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="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>
|
|
||||||
Сохранение...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<IconDeviceFloppy className="icon me-2" />
|
|
||||||
Сохранить в S3
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={handlePreviewDiff}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
Предпросмотр изменений
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={fetchItems}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<IconRefresh className="icon me-2" />
|
|
||||||
Обновить
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-primary"
|
|
||||||
onClick={handleImport}
|
|
||||||
>
|
|
||||||
<IconUpload className="icon me-2" />
|
|
||||||
Импорт из буфера
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-primary"
|
|
||||||
onClick={handleExport}
|
|
||||||
disabled={items.length === 0}
|
|
||||||
>
|
|
||||||
<IconDownload className="icon me-2" />
|
|
||||||
Экспорт CSV
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={clearInvalid}
|
|
||||||
disabled={items.length === 0}
|
|
||||||
>
|
|
||||||
Очистить пустые/невалидные
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Drag & Drop импорт */}
|
{/* Drag & Drop импорт */}
|
||||||
<div
|
<div
|
||||||
className="card card-md border-dashed"
|
className="card card-md border-dashed"
|
||||||
@@ -580,7 +528,7 @@ function DomainsNewManager() {
|
|||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('domain')}>
|
<th className="cursor-pointer" onClick={() => handleSort('domain')}>
|
||||||
Домен
|
Домен
|
||||||
{sortField === 'domain' && (
|
{sortField === 'domain' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
@@ -588,7 +536,7 @@ function DomainsNewManager() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('community')}>
|
<th className="cursor-pointer" onClick={() => handleSort('community')}>
|
||||||
Community
|
Community
|
||||||
{sortField === 'community' && (
|
{sortField === 'community' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
|
|||||||
@@ -1218,15 +1218,15 @@ function FilterManager() {
|
|||||||
onChange={toggleSelectAllProcessed}
|
onChange={toggleSelectAllProcessed}
|
||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
<th style={{ width: '25%', cursor: 'pointer' }} onClick={() => toggleSort('community')}>
|
<th style={{ width: '25%' }} className="cursor-pointer" onClick={() => toggleSort('community')}>
|
||||||
<span className="me-1">COMMUNITY</span>
|
<span className="me-1">COMMUNITY</span>
|
||||||
{sortBy === 'community' && (sortDir === 'asc' ? <IconSortAscending size={16} /> : <IconSortDescending size={16} />)}
|
{sortBy === 'community' && (sortDir === 'asc' ? <IconSortAscending size={16} /> : <IconSortDescending size={16} />)}
|
||||||
</th>
|
</th>
|
||||||
<th style={{ width: '25%', cursor: 'pointer' }} onClick={() => toggleSort('gateway')}>
|
<th style={{ width: '25%' }} className="cursor-pointer" onClick={() => toggleSort('gateway')}>
|
||||||
<span className="me-1">GATEWAY</span>
|
<span className="me-1">GATEWAY</span>
|
||||||
{sortBy === 'gateway' && (sortDir === 'asc' ? <IconSortAscending size={16} /> : <IconSortDescending size={16} />)}
|
{sortBy === 'gateway' && (sortDir === 'asc' ? <IconSortAscending size={16} /> : <IconSortDescending size={16} />)}
|
||||||
</th>
|
</th>
|
||||||
<th style={{ width: '35%', cursor: 'pointer' }} onClick={() => toggleSort('description')}>
|
<th style={{ width: '35%' }} className="cursor-pointer" onClick={() => toggleSort('description')}>
|
||||||
<span className="me-1">ОПИСАНИЕ</span>
|
<span className="me-1">ОПИСАНИЕ</span>
|
||||||
{sortBy === 'description' && (sortDir === 'asc' ? <IconSortAscending size={16} /> : <IconSortDescending size={16} />)}
|
{sortBy === 'description' && (sortDir === 'asc' ? <IconSortAscending size={16} /> : <IconSortDescending size={16} />)}
|
||||||
</th>
|
</th>
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ import {
|
|||||||
IconDeviceFloppy,
|
IconDeviceFloppy,
|
||||||
IconHash,
|
IconHash,
|
||||||
IconClock,
|
IconClock,
|
||||||
IconFileText
|
IconFileText,
|
||||||
|
IconEye
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import LockBanner from './components/LockBanner.jsx';
|
import LockBanner from './components/LockBanner.jsx';
|
||||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
import TableSkeleton from './components/TableSkeleton.jsx';
|
||||||
import S3MetaBar from './components/S3MetaBar.jsx';
|
import S3MetaBar from './components/S3MetaBar.jsx';
|
||||||
|
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
||||||
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
||||||
import HistoryModal from './components/HistoryModal.jsx';
|
import HistoryModal from './components/HistoryModal.jsx';
|
||||||
|
|
||||||
@@ -409,9 +411,21 @@ function IPRangesManager() {
|
|||||||
<div className="page-pretitle">Главная / Данные / IP ranges</div>
|
<div className="page-pretitle">Главная / Данные / IP ranges</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-auto ms-auto d-print-none">
|
<div className="col-auto ms-auto d-print-none">
|
||||||
<div className="btn-list">
|
<PageHeaderActions
|
||||||
<button className="btn btn-outline-secondary" onClick={() => setHistoryOpen(true)}>История</button>
|
loading={loading}
|
||||||
</div>
|
onPreview={handlePreviewDiff}
|
||||||
|
disablePreview={loading || items.length === 0}
|
||||||
|
onRefresh={fetchItems}
|
||||||
|
disableRefresh={loading}
|
||||||
|
onImport={handleImport}
|
||||||
|
onExport={handleExport}
|
||||||
|
disableExport={items.length === 0}
|
||||||
|
onClear={clearInvalid}
|
||||||
|
disableClear={items.length === 0}
|
||||||
|
onSave={handleSaveChanges}
|
||||||
|
disableSave={loading}
|
||||||
|
onHistory={() => setHistoryOpen(true)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -483,73 +497,7 @@ function IPRangesManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions Card */}
|
{/* Карточка действий больше не нужна — действия перенесены в page-header */}
|
||||||
<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="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>
|
|
||||||
Сохранение...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<IconDeviceFloppy className="icon me-2" />
|
|
||||||
Сохранить в S3
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={fetchItems}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<IconRefresh className="icon me-2" />
|
|
||||||
Обновить
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-primary"
|
|
||||||
onClick={handleImport}
|
|
||||||
>
|
|
||||||
<IconUpload className="icon me-2" />
|
|
||||||
Импорт из буфера
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-primary"
|
|
||||||
onClick={handleExport}
|
|
||||||
disabled={items.length === 0}
|
|
||||||
>
|
|
||||||
<IconDownload className="icon me-2" />
|
|
||||||
Экспорт CSV
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={handlePreviewDiff}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
Предпросмотр изменений
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
onClick={clearInvalid}
|
|
||||||
disabled={items.length === 0}
|
|
||||||
>
|
|
||||||
Очистить пустые/невалидные
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-lg-9">
|
<div className="col-lg-9">
|
||||||
@@ -588,7 +536,7 @@ function IPRangesManager() {
|
|||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('ipRange')}>
|
<th className="cursor-pointer" onClick={() => handleSort('ipRange')}>
|
||||||
IP-диапазон
|
IP-диапазон
|
||||||
{sortField === 'ipRange' && (
|
{sortField === 'ipRange' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
@@ -596,7 +544,7 @@ function IPRangesManager() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('community')}>
|
<th className="cursor-pointer" onClick={() => handleSort('community')}>
|
||||||
Community
|
Community
|
||||||
{sortField === 'community' && (
|
{sortField === 'community' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
|
|||||||
@@ -558,7 +558,7 @@ function ServerManager() {
|
|||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('ip')}>
|
<th className="cursor-pointer" onClick={() => handleSort('ip')}>
|
||||||
IP адрес
|
IP адрес
|
||||||
{sortField === 'ip' && (
|
{sortField === 'ip' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
@@ -566,7 +566,7 @@ function ServerManager() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('dns')}>
|
<th className="cursor-pointer" onClick={() => handleSort('dns')}>
|
||||||
DNS имя
|
DNS имя
|
||||||
{sortField === 'dns' && (
|
{sortField === 'dns' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
@@ -574,7 +574,7 @@ function ServerManager() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('country')}>
|
<th className="cursor-pointer" onClick={() => handleSort('country')}>
|
||||||
Страна
|
Страна
|
||||||
{sortField === 'country' && (
|
{sortField === 'country' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
@@ -582,7 +582,7 @@ function ServerManager() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('provider')}>
|
<th className="cursor-pointer" onClick={() => handleSort('provider')}>
|
||||||
Провайдер
|
Провайдер
|
||||||
{sortField === 'provider' && (
|
{sortField === 'provider' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
@@ -590,7 +590,7 @@ function ServerManager() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('tunnel')}>
|
<th className="cursor-pointer" onClick={() => handleSort('tunnel')}>
|
||||||
Туннель
|
Туннель
|
||||||
{sortField === 'tunnel' && (
|
{sortField === 'tunnel' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
@@ -598,7 +598,7 @@ function ServerManager() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('gateway')}>
|
<th className="cursor-pointer" onClick={() => handleSort('gateway')}>
|
||||||
Основной шлюз
|
Основной шлюз
|
||||||
{sortField === 'gateway' && (
|
{sortField === 'gateway' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import {
|
||||||
|
IconEye,
|
||||||
|
IconRefresh,
|
||||||
|
IconUpload,
|
||||||
|
IconDownload,
|
||||||
|
IconDeviceFloppy
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
|
function PageHeaderActions({
|
||||||
|
loading = false,
|
||||||
|
onPreview,
|
||||||
|
disablePreview = false,
|
||||||
|
onRefresh,
|
||||||
|
disableRefresh = false,
|
||||||
|
onImport,
|
||||||
|
onExport,
|
||||||
|
disableExport = false,
|
||||||
|
onClear,
|
||||||
|
disableClear = false,
|
||||||
|
onSave,
|
||||||
|
disableSave = false,
|
||||||
|
onHistory,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="btn-list">
|
||||||
|
{onPreview && (
|
||||||
|
<button className="btn btn-outline-secondary" onClick={onPreview} disabled={disablePreview} title="Предпросмотр изменений">
|
||||||
|
<IconEye className="me-1" /> Предпросмотр
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onRefresh && (
|
||||||
|
<button className="btn btn-outline-secondary" onClick={onRefresh} disabled={disableRefresh} title="Обновить данные">
|
||||||
|
<IconRefresh className={loading ? 'spin me-1' : 'me-1'} /> Обновить
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onImport && (
|
||||||
|
<button className="btn btn-outline-primary" onClick={onImport} title="Импорт">
|
||||||
|
<IconUpload className="me-1" /> Импорт
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onExport && (
|
||||||
|
<button className="btn btn-outline-primary" onClick={onExport} disabled={disableExport} title="Экспорт">
|
||||||
|
<IconDownload className="me-1" /> Экспорт
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onClear && (
|
||||||
|
<button className="btn btn-outline-secondary" onClick={onClear} disabled={disableClear} title="Очистить пустые/невалидные">
|
||||||
|
Очистить
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onSave && (
|
||||||
|
<button className="btn btn-primary" onClick={onSave} disabled={disableSave} title="Сохранить">
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<span className="spinner-border spinner-border-sm me-2" role="status"></span>
|
||||||
|
Сохранение...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<IconDeviceFloppy className="me-1" /> Сохранить
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onHistory && (
|
||||||
|
<button className="btn btn-outline-secondary" onClick={onHistory} title="История версий">История</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageHeaderActions;
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user