feat: Заменить рендеринг бейджей сообщества на компонент CommunityBadge в менеджерах ASNs, Domains и IPRanges, улучшив читаемость кода и унифицировав отображение. Добавить возможность скачивания шаблонов CSV для пустых состояний в этих менеджерах, улучшая пользовательский опыт. Обновить модальные окна подтверждения удаления для более удобного взаимодействия с пользователем.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m52s

This commit is contained in:
2025-08-27 19:48:58 +07:00
parent ac0cbc8c31
commit 730017405c
8 changed files with 171 additions and 174 deletions
+34 -56
View File
@@ -1,6 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import api from './lib/api.js';
import CommunityAutocompleteInput from './components/CommunityAutocompleteInput.jsx';
import CommunityBadge from './components/CommunityBadge.jsx';
import {
IconPlus,
IconSearch,
@@ -74,17 +75,7 @@ function IPRangesManager() {
})();
}, []);
const renderCommunityBadge = (value) => {
const v = String(value ?? '').trim();
if (!v) return <span className="badge bg-blue-lt text-blue"></span>;
const meta = communities.find(c => c.value === v);
const color = (meta?.color || 'blue').toLowerCase().replace(/[^a-z-]/g, '');
const cls = `badge bg-${color}-lt text-${color}`;
const title = meta ? `${meta.name ? meta.name + ' — ' : ''}${meta.description || ''}${meta.tags && meta.tags.length ? ' (' + meta.tags.join(', ') + ')' : ''}` : '';
return (
<span className={cls} title={title}>{v}</span>
);
};
const renderCommunityBadge = (value) => (<CommunityBadge value={value} communities={communities} />);
const isValidIPv4 = (ip) => {
const octets = ip.split('.');
@@ -364,6 +355,21 @@ function IPRangesManager() {
setImportOpen(false);
};
// Шаблон CSV для пустого состояния
const downloadSampleCsv = () => {
const header = ['ipRange','community'];
const lines = [header, ['192.168.1.0/24', '65000:100']]
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
.join('\n');
const blob = new Blob([lines], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'ip_ranges_sample.csv';
a.click();
URL.revokeObjectURL(url);
};
// Сортировка
const sortedItems = [...items].sort((a, b) => {
let valA = a[sortField] || '';
@@ -878,7 +884,12 @@ function IPRangesManager() {
<TableSkeleton rows={10} cols={3} />
) : paginatedItems.length === 0 ? (
<TableEmpty cols={3}>
<EmptyState title="Нет IP-диапазонов" description="Импортируйте или добавьте записи, чтобы начать." action={<button className="btn btn-primary" onClick={handleImport}>Импорт</button>} />
<EmptyState
title="Нет IP-диапазонов"
description="Импортируйте или добавьте записи, чтобы начать."
action={<button className="btn btn-primary" onClick={handleImport}>Импорт</button>}
secondaryAction={<button className="btn btn-outline-primary" onClick={downloadSampleCsv}>Шаблон CSV</button>}
/>
</TableEmpty>
) : (
<div className="table-responsive">
@@ -986,50 +997,17 @@ function IPRangesManager() {
</div>
</div>
{/* Delete Confirmation Modal */}
{showDeleteModal && (
<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>Удалить IP-диапазон?</h3>
<div className="text-muted">
Вы уверены, что хотите удалить "{itemToDelete?.ipRange}"? Это действие необратимо.
</div>
</div>
<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>
</div>
)}
<ConfirmDialog
open={showDeleteModal}
title={'Удалить IP-диапазон?'}
message={!itemToDelete ? '' : `Вы уверены, что хотите удалить "${itemToDelete.ipRange}"? Это действие необратимо.`}
confirmText={'Удалить'}
cancelText={'Отмена'}
destructive
size={'sm'}
onConfirm={executeDelete}
onCancel={() => setShowDeleteModal(false)}
/>
<ConfirmDiffModal
show={confirmSaveOpen}
diff={diff}