refactor(FilterManager): simplify notification handling and improve layout structure for enhanced user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m0s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m0s
This commit is contained in:
+166
-128
@@ -1740,32 +1740,32 @@ function FilterManager() {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-wrapper">
|
<>
|
||||||
|
{/* Уведомления */}
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-danger alert-dismissible" role="alert">
|
||||||
|
<IconAlertTriangle className="me-2" />
|
||||||
|
{error}
|
||||||
|
<button type="button" className="btn-close" onClick={() => setError('')}></button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{success && (
|
||||||
|
<div className="alert alert-success alert-dismissible" role="alert">
|
||||||
|
<IconCheck className="me-2" />
|
||||||
|
{success}
|
||||||
|
<button type="button" className="btn-close" onClick={() => setSuccess('')}></button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Управление фильтрами frouting"
|
title="Управление фильтрами frouting"
|
||||||
icon={<IconFilter size={24} />}
|
icon={<IconFilter size={24} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="page-body">
|
<div className="row">
|
||||||
<div className="container-xl">
|
<div className="col-12">
|
||||||
{/* Уведомления */}
|
{/* Селектор серверов на одной странице (Tabler UI) */}
|
||||||
{error && (
|
<div className="card mb-3">
|
||||||
<div className="alert alert-danger alert-dismissible" role="alert">
|
|
||||||
<IconAlertTriangle className="me-2" />
|
|
||||||
{error}
|
|
||||||
<button type="button" className="btn-close" onClick={() => setError('')}></button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{success && (
|
|
||||||
<div className="alert alert-success alert-dismissible" role="alert">
|
|
||||||
<IconCheck className="me-2" />
|
|
||||||
{success}
|
|
||||||
<button type="button" className="btn-close" onClick={() => setSuccess('')}></button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Селектор серверов на одной странице (Tabler UI) */}
|
|
||||||
<div className="card mb-3">
|
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<h3 className="card-title d-flex align-items-center gap-2">
|
<h3 className="card-title d-flex align-items-center gap-2">
|
||||||
<IconServer className="icon" />
|
<IconServer className="icon" />
|
||||||
@@ -1976,9 +1976,9 @@ function FilterManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Секция фильтров выбранного сервера */}
|
{/* Секция фильтров выбранного сервера */}
|
||||||
{selectedServer && (
|
{selectedServer && (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
@@ -2051,113 +2051,151 @@ function FilterManager() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="alert alert-info mb-3">
|
<div className="alert alert-info mb-3">
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<IconGripVertical size={18} className="me-2" />
|
<IconGripVertical size={18} className="me-2" />
|
||||||
<span>Перетащите строки за иконку <IconGripVertical size={14} className="mx-1" /> чтобы изменить приоритет</span>
|
<span>
|
||||||
</div>
|
Перетащите строки за иконку{' '}
|
||||||
<div className="text-muted small mt-1">
|
<IconGripVertical size={14} className="mx-1" /> чтобы изменить приоритет
|
||||||
<strong>Важно:</strong> В RouterOS правила внизу списка имеют <strong>высший приоритет</strong> (переопределяют верхние)
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="text-muted small mt-1">
|
||||||
<DndContext
|
<strong>Важно:</strong> В RouterOS правила внизу списка имеют{' '}
|
||||||
sensors={sensors}
|
<strong>высший приоритет</strong> (переопределяют верхние)
|
||||||
collisionDetection={closestCenter}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
>
|
|
||||||
<div className="table-responsive">
|
|
||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style={{ width: '40px' }}></th>
|
|
||||||
<th className="w-1">
|
|
||||||
<input
|
|
||||||
className="form-check-input m-0 align-middle"
|
|
||||||
type="checkbox"
|
|
||||||
aria-label="Выбрать все"
|
|
||||||
checked={areAllProcessedSelected}
|
|
||||||
onChange={toggleSelectAllProcessed}
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
<th style={{ width: '60px' }}>#</th>
|
|
||||||
<th style={{ width: '22%' }} className="cursor-pointer" onClick={() => toggleSort('community')}>
|
|
||||||
<span className="me-1">COMMUNITY</span>
|
|
||||||
{sortBy === 'community' && (sortDir === 'asc' ? <IconSortAscending size={16} /> : <IconSortDescending size={16} />)}
|
|
||||||
</th>
|
|
||||||
<th style={{ width: '22%' }} className="cursor-pointer" onClick={() => toggleSort('gateway')}>
|
|
||||||
<span className="me-1">GATEWAY</span>
|
|
||||||
{sortBy === 'gateway' && (sortDir === 'asc' ? <IconSortAscending size={16} /> : <IconSortDescending size={16} />)}
|
|
||||||
</th>
|
|
||||||
<th style={{ width: '30%' }} className="cursor-pointer" onClick={() => toggleSort('description')}>
|
|
||||||
<span className="me-1">ОПИСАНИЕ</span>
|
|
||||||
{sortBy === 'description' && (sortDir === 'asc' ? <IconSortAscending size={16} /> : <IconSortDescending size={16} />)}
|
|
||||||
</th>
|
|
||||||
<th style={{ width: '120px' }} className="text-end">ДЕЙСТВИЯ</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<SortableContext
|
|
||||||
items={serverFilters.filter(f => f != null).map(f => filterKey(f))}
|
|
||||||
strategy={verticalListSortingStrategy}
|
|
||||||
>
|
|
||||||
<tbody>
|
|
||||||
{serverFilters.filter(f => f != null).map((filter, index) => {
|
|
||||||
const key = filterKey(filter);
|
|
||||||
const isSelected = selectedFilterKeys.has(key);
|
|
||||||
// Показываем только если совпадает с поиском
|
|
||||||
if (filterSearch.trim()) {
|
|
||||||
const search = filterSearch.trim().toLowerCase();
|
|
||||||
const c = String(filter.community || '').toLowerCase();
|
|
||||||
const g = String(filter.gateway || '').toLowerCase();
|
|
||||||
const d = String(filter.description || '').toLowerCase();
|
|
||||||
if (!c.includes(search) && !g.includes(search) && !d.includes(search)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<SortableFilterRow
|
|
||||||
key={key}
|
|
||||||
filter={filter}
|
|
||||||
index={index}
|
|
||||||
totalCount={serverFilters.filter(f => f != null).length}
|
|
||||||
filterKey={key}
|
|
||||||
isSelected={isSelected}
|
|
||||||
onToggleSelect={() => toggleSelectFilter(key)}
|
|
||||||
onEdit={() => handleEditFilter(filter, index)}
|
|
||||||
onDelete={() => {
|
|
||||||
setFilterToDelete(filter);
|
|
||||||
setDeleteFilterModalOpen(true);
|
|
||||||
}}
|
|
||||||
communities={communities}
|
|
||||||
baseAS={baseAS}
|
|
||||||
selectedServerGateways={selectedServerGateways}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</SortableContext>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</DndContext>
|
|
||||||
{selectedFilterKeys.size > 0 && (
|
|
||||||
<div className="d-flex justify-content-between align-items-center mt-3">
|
|
||||||
<div className="text-muted">Выбрано: {selectedFilterKeys.size}</div>
|
|
||||||
<div className="btn-list">
|
|
||||||
<button className="btn btn-outline-danger btn-sm" onClick={deleteSelectedFilters}>
|
|
||||||
<IconTrash className="me-2" /> Удалить выбранные
|
|
||||||
</button>
|
|
||||||
<button className="btn btn-outline-secondary btn-sm" onClick={() => setSelectedFilterKeys(new Set())}>
|
|
||||||
<IconX className="me-2" /> Отменить выбор
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<DndContext
|
||||||
</>
|
sensors={sensors}
|
||||||
)}
|
collisionDetection={closestCenter}
|
||||||
</div>
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: '40px' }}></th>
|
||||||
|
<th className="w-1">
|
||||||
|
<input
|
||||||
|
className="form-check-input m-0 align-middle"
|
||||||
|
type="checkbox"
|
||||||
|
aria-label="Выбрать все"
|
||||||
|
checked={areAllProcessedSelected}
|
||||||
|
onChange={toggleSelectAllProcessed}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th style={{ width: '60px' }}>#</th>
|
||||||
|
<th
|
||||||
|
style={{ width: '22%' }}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => toggleSort('community')}
|
||||||
|
>
|
||||||
|
<span className="me-1">COMMUNITY</span>
|
||||||
|
{sortBy === 'community' &&
|
||||||
|
(sortDir === 'asc' ? (
|
||||||
|
<IconSortAscending size={16} />
|
||||||
|
) : (
|
||||||
|
<IconSortDescending size={16} />
|
||||||
|
))}
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{ width: '22%' }}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => toggleSort('gateway')}
|
||||||
|
>
|
||||||
|
<span className="me-1">GATEWAY</span>
|
||||||
|
{sortBy === 'gateway' &&
|
||||||
|
(sortDir === 'asc' ? (
|
||||||
|
<IconSortAscending size={16} />
|
||||||
|
) : (
|
||||||
|
<IconSortDescending size={16} />
|
||||||
|
))}
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
style={{ width: '30%' }}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={() => toggleSort('description')}
|
||||||
|
>
|
||||||
|
<span className="me-1">ОПИСАНИЕ</span>
|
||||||
|
{sortBy === 'description' &&
|
||||||
|
(sortDir === 'asc' ? (
|
||||||
|
<IconSortAscending size={16} />
|
||||||
|
) : (
|
||||||
|
<IconSortDescending size={16} />
|
||||||
|
))}
|
||||||
|
</th>
|
||||||
|
<th style={{ width: '120px' }} className="text-end">
|
||||||
|
ДЕЙСТВИЯ
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<SortableContext
|
||||||
|
items={serverFilters.filter((f) => f != null).map((f) => filterKey(f))}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
{serverFilters
|
||||||
|
.filter((f) => f != null)
|
||||||
|
.map((filter, index) => {
|
||||||
|
const key = filterKey(filter);
|
||||||
|
const isSelected = selectedFilterKeys.has(key);
|
||||||
|
// Показываем только если совпадает с поиском
|
||||||
|
if (filterSearch.trim()) {
|
||||||
|
const search = filterSearch.trim().toLowerCase();
|
||||||
|
const c = String(filter.community || '').toLowerCase();
|
||||||
|
const g = String(filter.gateway || '').toLowerCase();
|
||||||
|
const d = String(filter.description || '').toLowerCase();
|
||||||
|
if (!c.includes(search) && !g.includes(search) && !d.includes(search)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<SortableFilterRow
|
||||||
|
key={key}
|
||||||
|
filter={filter}
|
||||||
|
index={index}
|
||||||
|
totalCount={serverFilters.filter((f) => f != null).length}
|
||||||
|
filterKey={key}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onToggleSelect={() => toggleSelectFilter(key)}
|
||||||
|
onEdit={() => handleEditFilter(filter, index)}
|
||||||
|
onDelete={() => {
|
||||||
|
setFilterToDelete(filter);
|
||||||
|
setDeleteFilterModalOpen(true);
|
||||||
|
}}
|
||||||
|
communities={communities}
|
||||||
|
baseAS={baseAS}
|
||||||
|
selectedServerGateways={selectedServerGateways}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</SortableContext>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</DndContext>
|
||||||
|
{selectedFilterKeys.size > 0 && (
|
||||||
|
<div className="d-flex justify-content-between align-items-center mt-3">
|
||||||
|
<div className="text-muted">Выбрано: {selectedFilterKeys.size}</div>
|
||||||
|
<div className="btn-list">
|
||||||
|
<button className="btn btn-outline-danger btn-sm" onClick={deleteSelectedFilters}>
|
||||||
|
<IconTrash className="me-2" /> Удалить выбранные
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-secondary btn-sm"
|
||||||
|
onClick={() => setSelectedFilterKeys(new Set())}
|
||||||
|
>
|
||||||
|
<IconX className="me-2" /> Отменить выбор
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Модальные окна */}
|
{/* Модальные окна */}
|
||||||
<FormModal
|
<FormModal
|
||||||
@@ -2635,7 +2673,7 @@ function FilterManager() {
|
|||||||
onCancel={() => setConfirmState({ open: false, text: '', onConfirm: null })}
|
onCancel={() => setConfirmState({ open: false, text: '', onConfirm: null })}
|
||||||
onConfirm={() => confirmState.onConfirm?.()}
|
onConfirm={() => confirmState.onConfirm?.()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user