feat: Обновление компонента AutoUrlManager с добавлением новых функций, таких как импорт URL и очистка пустых строк. Внедрены новые компоненты для улучшения пользовательского интерфейса и взаимодействия, а также улучшена валидация и обработка ошибок.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
This commit is contained in:
+134
-104
@@ -13,6 +13,11 @@ import {
|
|||||||
IconCircleCheck,
|
IconCircleCheck,
|
||||||
IconCopy
|
IconCopy
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
import PageHeader from './components/PageHeader.jsx';
|
||||||
|
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
||||||
|
import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx';
|
||||||
|
import EmptyState from './components/EmptyState.jsx';
|
||||||
|
import ImportModal from './components/ImportModal.jsx';
|
||||||
|
|
||||||
function AutoUrlManager() {
|
function AutoUrlManager() {
|
||||||
const [urls, setUrls] = useState([]);
|
const [urls, setUrls] = useState([]);
|
||||||
@@ -22,6 +27,7 @@ function AutoUrlManager() {
|
|||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [messageType, setMessageType] = useState('');
|
const [messageType, setMessageType] = useState('');
|
||||||
const [touched, setTouched] = useState({});
|
const [touched, setTouched] = useState({});
|
||||||
|
const [importOpen, setImportOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUrls();
|
fetchUrls();
|
||||||
@@ -45,6 +51,10 @@ function AutoUrlManager() {
|
|||||||
setUrls([...urls, { url: '', community: '' }]);
|
setUrls([...urls, { url: '', community: '' }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearEmptyRows = () => {
|
||||||
|
setUrls(urls.filter(u => u != null && (u.url || u.community)));
|
||||||
|
};
|
||||||
|
|
||||||
const removeUrl = (index) => {
|
const removeUrl = (index) => {
|
||||||
setUrls(urls.filter((_, i) => i !== index));
|
setUrls(urls.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
@@ -144,24 +154,83 @@ function AutoUrlManager() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="d-flex justify-content-center align-items-center" style={{ minHeight: '200px' }}>
|
|
||||||
<div className="spinner-border" role="status" aria-label="Загрузка"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="page-header d-print-none mb-4">
|
<PageHeader
|
||||||
<div className="row align-items-center">
|
title="Автоматические URL"
|
||||||
<div className="col">
|
pretitle="Управление автоматическими URL-адресами для загрузки списков IP и доменов"
|
||||||
<h2 className="page-title">Автоматические URL</h2>
|
actions={(
|
||||||
<div className="page-pretitle">Управление автоматическими URL-адресами для загрузки списков IP и доменов</div>
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<div className="btn-list">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setImportOpen(true)}
|
||||||
|
disabled={saving || processing}
|
||||||
|
title="Импорт из текста или файла"
|
||||||
|
>
|
||||||
|
<IconUpload className="me-1" /> Импорт
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
type="button"
|
||||||
|
onClick={addUrl}
|
||||||
|
disabled={saving || processing}
|
||||||
|
title="Добавить строку"
|
||||||
|
>
|
||||||
|
<IconPlus className="me-1" /> Добавить URL
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="ms-auto btn-list">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={clearEmptyRows}
|
||||||
|
disabled={saving || processing || urls.length === 0}
|
||||||
|
title="Удалить пустые строки"
|
||||||
|
>
|
||||||
|
Очистить пустые
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
type="button"
|
||||||
|
onClick={saveUrls}
|
||||||
|
disabled={saving || processing || !hasAtLeastOneValidRow}
|
||||||
|
title="Сохранить список URL"
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<>
|
||||||
|
<span className="spinner-border spinner-border-sm me-2" role="status" />
|
||||||
|
Сохранение...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<IconDeviceFloppy className="me-1" /> Сохранить
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
type="button"
|
||||||
|
onClick={processUrls}
|
||||||
|
disabled={saving || processing || !hasAtLeastOneValidRow}
|
||||||
|
title="Загрузить и обработать все URL"
|
||||||
|
>
|
||||||
|
{processing ? (
|
||||||
|
<>
|
||||||
|
<span className="spinner-border spinner-border-sm me-2" role="status" />
|
||||||
|
Обработка...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<IconDownload className="me-1" /> Загрузить списки
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{message && (
|
{message && (
|
||||||
<div className={`alert alert-${messageType === 'error' ? 'danger' : 'success'} alert-dismissible`} role="alert">
|
<div className={`alert alert-${messageType === 'error' ? 'danger' : 'success'} alert-dismissible`} role="alert">
|
||||||
@@ -174,53 +243,36 @@ function AutoUrlManager() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
|
||||||
<h3 className="card-title">URL-адреса для автоматической загрузки</h3>
|
|
||||||
<div className="card-actions">
|
|
||||||
<div className="btn-list">
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-secondary btn-sm"
|
|
||||||
onClick={() => {
|
|
||||||
const text = window.prompt('Вставьте строки вида: URL ПРОБЕЛ COMMUNITY (по одной записи на строку)');
|
|
||||||
if (!text) return;
|
|
||||||
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
||||||
const parsed = lines.map(l => {
|
|
||||||
const [u, c] = l.split(/\s+/);
|
|
||||||
return { url: u || '', community: c || '' };
|
|
||||||
});
|
|
||||||
setUrls(prev => [...prev, ...parsed]);
|
|
||||||
}}
|
|
||||||
disabled={saving || processing}
|
|
||||||
title="Массовое добавление из буфера"
|
|
||||||
>
|
|
||||||
<IconUpload className="me-1" />
|
|
||||||
Импорт
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary btn-sm"
|
|
||||||
onClick={addUrl}
|
|
||||||
disabled={saving || processing}
|
|
||||||
>
|
|
||||||
<IconPlus className="me-1" />
|
|
||||||
Добавить URL
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{urls.length === 0 ? (
|
{loading ? (
|
||||||
<div className="text-center text-muted py-4">
|
<TableSkeleton rows={6} cols={3} />
|
||||||
<IconAlertCircle size={48} className="mb-3" />
|
) : urls.length === 0 ? (
|
||||||
<p>Нет добавленных URL-адресов</p>
|
<EmptyState
|
||||||
<button
|
icon={IconAlertCircle}
|
||||||
className="btn btn-primary"
|
title="Нет добавленных URL-адресов"
|
||||||
onClick={addUrl}
|
description="Добавьте хотя бы одну строку с корректным URL и числовым community."
|
||||||
disabled={saving || processing}
|
size="default"
|
||||||
>
|
action={(
|
||||||
<IconPlus className="me-1" />
|
<button
|
||||||
Добавить первый URL
|
className="btn btn-primary"
|
||||||
</button>
|
onClick={addUrl}
|
||||||
</div>
|
disabled={saving || processing}
|
||||||
|
>
|
||||||
|
<IconPlus className="me-1" />
|
||||||
|
Добавить первый URL
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
secondaryAction={(
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={() => setImportOpen(true)}
|
||||||
|
disabled={saving || processing}
|
||||||
|
>
|
||||||
|
<IconUpload className="me-1" />
|
||||||
|
Импорт
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
@@ -299,49 +351,8 @@ function AutoUrlManager() {
|
|||||||
<span className="badge bg-blue-lt text-blue">Всего URL-адресов: {urls.length}</span>
|
<span className="badge bg-blue-lt text-blue">Всего URL-адресов: {urls.length}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="text-muted small">
|
||||||
<button
|
Введите http/https URL и числовой community. Не валидные поля подсвечиваются.
|
||||||
className="btn btn-outline-secondary me-2"
|
|
||||||
onClick={() => setUrls(urls.filter(u => u != null && (u.url || u.community)))}
|
|
||||||
disabled={saving || processing || urls.length === 0}
|
|
||||||
title="Удалить пустые строки"
|
|
||||||
>
|
|
||||||
Очистить пустые
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-primary me-2"
|
|
||||||
onClick={saveUrls}
|
|
||||||
disabled={saving || processing || !hasAtLeastOneValidRow}
|
|
||||||
>
|
|
||||||
{saving ? (
|
|
||||||
<>
|
|
||||||
<span className="spinner-border spinner-border-sm me-2" role="status" />
|
|
||||||
Сохранение...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<IconDeviceFloppy className="me-1" />
|
|
||||||
Сохранить
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={processUrls}
|
|
||||||
disabled={saving || processing || !hasAtLeastOneValidRow}
|
|
||||||
>
|
|
||||||
{processing ? (
|
|
||||||
<>
|
|
||||||
<span className="spinner-border spinner-border-sm me-2" role="status" />
|
|
||||||
Обработка...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<IconDownload className="me-1" />
|
|
||||||
Загрузить списки
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -405,6 +416,25 @@ function AutoUrlManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Модальное окно импорта */}
|
||||||
|
<ImportModal
|
||||||
|
show={importOpen}
|
||||||
|
title="Импорт URL + community"
|
||||||
|
description="Формат строк: URL ПРОБЕЛ COMMUNITY. Например: https://example.com/ips.txt 555"
|
||||||
|
sampleHeader={['url', 'community']}
|
||||||
|
placeholder={'https://example.com/ips.txt 555\nhttps://test.com/domains.txt 666'}
|
||||||
|
parseLine={(line) => {
|
||||||
|
const [u, c] = String(line).split(/\s+/);
|
||||||
|
return { url: u || '', community: c || '' };
|
||||||
|
}}
|
||||||
|
validateItem={(obj) => Boolean(obj?.url) && Boolean(obj?.community)}
|
||||||
|
onConfirm={(items) => {
|
||||||
|
setUrls(prev => [...prev, ...items]);
|
||||||
|
setImportOpen(false);
|
||||||
|
}}
|
||||||
|
onClose={() => setImportOpen(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user