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
+25 -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,
@@ -78,17 +79,7 @@ function ASNsNewManager() {
// Подгружаем имена ASN из кэша/внешних API (эффект будет размещен ниже после определения paginatedItems)
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 isValidAsn = (value) => /^[0-9]+$/.test(String(value).trim());
// Допускаем как числовые, так и строковые (AS:NNN) community
@@ -635,7 +626,18 @@ function ASNsNewManager() {
<TableSkeleton rows={10} cols={3} />
) : paginatedItems.length === 0 ? (
<TableEmpty cols={3}>
<EmptyState title="Нет ASN" description="Импортируйте или добавьте записи, чтобы начать." action={<button className="btn btn-primary" onClick={handleImport}>Импорт</button>} />
<EmptyState
title="Нет ASN"
description="Импортируйте или добавьте записи, чтобы начать."
action={<button className="btn btn-primary" onClick={handleImport}>Импорт</button>}
secondaryAction={<button className="btn btn-outline-primary" onClick={() => {
const header = ['asn','community'];
const lines = [header, ['12345','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 = 'asns_sample.csv'; a.click(); URL.revokeObjectURL(url);
}}>Шаблон CSV</button>}
/>
</TableEmpty>
) : (
<div className="table-responsive">
@@ -748,50 +750,17 @@ function ASNsNewManager() {
</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>Удалить ASN?</h3>
<div className="text-muted">
Вы уверены, что хотите удалить "{itemToDelete?.asn}"? Это действие необратимо.
</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={'Удалить ASN?'}
message={!itemToDelete ? '' : `Вы уверены, что хотите удалить "${itemToDelete.asn}"? Это действие необратимо.`}
confirmText={'Удалить'}
cancelText={'Отмена'}
destructive
size={'sm'}
onConfirm={executeDelete}
onCancel={() => setShowDeleteModal(false)}
/>
<ConfirmDiffModal
show={confirmSaveOpen}
diff={diff}