feat: Заменить рендеринг бейджей сообщества на компонент CommunityBadge в менеджерах ASNs, Domains и IPRanges, улучшив читаемость кода и унифицировав отображение. Добавить возможность скачивания шаблонов CSV для пустых состояний в этих менеджерах, улучшая пользовательский опыт. Обновить модальные окна подтверждения удаления для более удобного взаимодействия с пользователем.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m52s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m52s
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user