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:
@@ -0,0 +1,21 @@
|
||||
function normalizeTablerColor(input) {
|
||||
const allowed = new Set(['blue','azure','indigo','purple','pink','red','orange','yellow','lime','green','teal','cyan','grape','gray']);
|
||||
const c = String(input || 'blue').toLowerCase().replace(/[^a-z-]/g, '');
|
||||
return allowed.has(c) ? c : 'blue';
|
||||
}
|
||||
|
||||
function CommunityBadge({ value, communities = [] }) {
|
||||
const v = String(value ?? '').trim();
|
||||
if (!v) return <span className="badge bg-blue-lt text-blue">—</span>;
|
||||
const meta = communities.find(c => String(c?.value) === v);
|
||||
const color = normalizeTablerColor(meta?.color || 'blue');
|
||||
const cls = `badge bg-${color}-lt text-${color}`;
|
||||
const title = meta ? `${meta.name ? meta.name + ' — ' : ''}${meta.description || ''}${Array.isArray(meta.tags) && meta.tags.length ? ' (' + meta.tags.join(', ') + ')' : ''}` : '';
|
||||
return (
|
||||
<span className={cls} title={title}>{v}</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default CommunityBadge;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export default function ConfirmDialog({ open, title, message, confirmText = 'Подтвердить', cancelText = 'Отмена', onConfirm, onCancel }) {
|
||||
export default function ConfirmDialog({ open, title, message, confirmText = 'Подтвердить', cancelText = 'Отмена', onConfirm, onCancel, destructive = false, size = 'md' }) {
|
||||
const ref = useRef(null)
|
||||
useEffect(() => {
|
||||
if (open && ref.current) {
|
||||
@@ -10,7 +10,7 @@ export default function ConfirmDialog({ open, title, message, confirmText = 'П
|
||||
if (!open) return null
|
||||
return (
|
||||
<div className="modal show d-block" role="dialog" aria-modal="true" aria-labelledby="confirm-title" aria-describedby="confirm-message" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }} onKeyDown={(e) => { if (e.key === 'Escape') onCancel?.() }}>
|
||||
<div className="modal-dialog" role="document">
|
||||
<div className={`modal-dialog ${size === 'sm' ? 'modal-sm' : size === 'lg' ? 'modal-lg' : ''}`} role="document">
|
||||
<div className="modal-content" ref={ref} tabIndex={-1} onKeyDown={(e) => {
|
||||
if (e.key === 'Tab') {
|
||||
const focusable = ref.current?.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
||||
@@ -30,7 +30,7 @@ export default function ConfirmDialog({ open, title, message, confirmText = 'П
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" onClick={onCancel}>{cancelText}</button>
|
||||
<button type="button" className="btn btn-primary" data-primary onClick={onConfirm}>{confirmText}</button>
|
||||
<button type="button" className={`btn ${destructive ? 'btn-danger' : 'btn-primary'}`} data-primary onClick={onConfirm}>{confirmText}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ function EmptyState({
|
||||
title = 'Нет данных',
|
||||
description,
|
||||
action,
|
||||
secondaryAction,
|
||||
}) {
|
||||
return (
|
||||
<div className="empty">
|
||||
@@ -17,9 +18,12 @@ function EmptyState({
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{action && (
|
||||
{(action || secondaryAction) && (
|
||||
<div className="empty-action">
|
||||
{action}
|
||||
{secondaryAction && (
|
||||
<span className="ms-2">{secondaryAction}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ function PageHeaderActions({
|
||||
onOnlineUpdate,
|
||||
onBackgroundUpdate,
|
||||
}) {
|
||||
const secondaryButtons = Boolean(onImport || onExport || onClear || onClearCommunities || onHistory);
|
||||
const secondaryButtons = Boolean(onPreview || onImport || onExport || onClear || onClearCommunities || onHistory);
|
||||
// Рендер плейсхолдеров во время загрузки/перезагрузки страницы
|
||||
if (loading) {
|
||||
const showUpdateGroup = Boolean(onBackgroundUpdate || onOnlineUpdate);
|
||||
@@ -176,6 +176,11 @@ function PageHeaderActions({
|
||||
align="left"
|
||||
buttonContent={<><IconDots className="me-1" /> Ещё</>}
|
||||
>
|
||||
{onPreview && (
|
||||
<button className="dropdown-item" type="button" onClick={onPreview} disabled={disablePreview}>
|
||||
<IconEye className="me-1" /> Предпросмотр
|
||||
</button>
|
||||
)}
|
||||
{onRefresh && (
|
||||
<button className="dropdown-item" type="button" onClick={onRefresh} disabled={disableRefresh}>
|
||||
<IconRefresh className="me-1" /> Обновить
|
||||
|
||||
Reference in New Issue
Block a user