feat(FirewallPage): enhance apply summary functionality with confirmation for individual and bulk actions
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m8s

This commit is contained in:
2026-02-21 19:32:17 +07:00
parent ef7a4817c9
commit 56f1baa7ba
+67 -18
View File
@@ -231,6 +231,8 @@ export default function FirewallPage() {
const [applying, setApplying] = useState(false); const [applying, setApplying] = useState(false);
const [ipInfoMap, setIpInfoMap] = useState({}); const [ipInfoMap, setIpInfoMap] = useState({});
const [ipInfoLoading, setIpInfoLoading] = useState(false); const [ipInfoLoading, setIpInfoLoading] = useState(false);
const [applyConfirmRow, setApplyConfirmRow] = useState(null);
const [applyingRow, setApplyingRow] = useState(null);
const routerServers = useMemo( const routerServers = useMemo(
() => () =>
@@ -327,26 +329,46 @@ export default function FirewallPage() {
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [analysisResult?.suggestions]); }, [analysisResult?.suggestions]);
const applyPayload = applyConfirmRow
? { removeIds: applyConfirmRow.ids, addEntries: [applyConfirmRow.addEntry] }
: analysisResult?.suggestions?.length
? {
removeIds: analysisResult.suggestions.flatMap((s) => s.ids),
addEntries: analysisResult.suggestions.map((s) => s.addEntry),
}
: null;
const handleApplySummary = async () => { const handleApplySummary = async () => {
if (!serverId || !analysisResult?.suggestions?.length) return; if (!serverId || !applyPayload) return;
setApplying(true); const isRow = !!applyConfirmRow;
if (isRow) setApplyingRow(applyConfirmRow.subnet);
else setApplying(true);
try { try {
const removeIds = analysisResult.suggestions.flatMap((s) => s.ids);
const addEntries = analysisResult.suggestions.map((s) => s.addEntry);
await api.post('/mikrotik/address-lists/apply-summary', { await api.post('/mikrotik/address-lists/apply-summary', {
serverId, serverId,
removeIds, removeIds: applyPayload.removeIds,
addEntries, addEntries: applyPayload.addEntries,
}); });
window.notify?.success?.(`Удалено ${removeIds.length} записей, добавлено ${addEntries.length} подсетей.`); const removed = applyPayload.removeIds.length;
const added = applyPayload.addEntries.length;
window.notify?.success?.(`Удалено ${removed} записей, добавлено ${added} подсетей.`);
setApplyConfirmOpen(false); setApplyConfirmOpen(false);
setAnalysisOpen(false); setApplyConfirmRow(null);
setAnalysisResult(null); if (isRow && analysisResult?.suggestions) {
setAnalysisResult((prev) => ({
...prev,
suggestions: prev.suggestions.filter((s) => s.subnet !== applyConfirmRow.subnet),
}));
} else {
setAnalysisOpen(false);
setAnalysisResult(null);
}
await fetchAddressLists(); await fetchAddressLists();
} catch (e) { } catch (e) {
window.notify?.error?.(e?.response?.data?.message || e?.message || 'Ошибка применения'); window.notify?.error?.(e?.response?.data?.message || e?.message || 'Ошибка применения');
} finally { } finally {
setApplying(false); setApplying(false);
setApplyingRow(null);
} }
}; };
@@ -457,10 +479,11 @@ export default function FirewallPage() {
setAnalysisOpen(false); setAnalysisOpen(false);
setAnalysisResult(null); setAnalysisResult(null);
setApplyConfirmOpen(false); setApplyConfirmOpen(false);
setApplyConfirmRow(null);
setIpInfoMap({}); setIpInfoMap({});
}} }}
title="Анализ списка ban" title="Анализ списка ban"
size="lg" size="xl"
> >
<div className="mb-3"> <div className="mb-3">
<label className="form-label">Суммаризировать подсети /24, в которых одиночных IP больше чем</label> <label className="form-label">Суммаризировать подсети /24, в которых одиночных IP больше чем</label>
@@ -504,12 +527,14 @@ export default function FirewallPage() {
<th>AS / Хостер</th> <th>AS / Хостер</th>
<th>Ссылка</th> <th>Ссылка</th>
<th>Действие</th> <th>Действие</th>
<th className="text-end" style={{ width: '120px' }}>Применить</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{analysisResult.suggestions.map((s) => { {analysisResult.suggestions.map((s) => {
const firstIp = s.subnet.split('/')[0]; const firstIp = s.subnet.split('/')[0];
const info = ipInfoMap[s.subnet]; const info = ipInfoMap[s.subnet];
const rowApplying = applyingRow === s.subnet;
return ( return (
<tr key={s.subnet}> <tr key={s.subnet}>
<td className="font-monospace">{s.subnet}</td> <td className="font-monospace">{s.subnet}</td>
@@ -532,6 +557,22 @@ export default function FirewallPage() {
</a> </a>
</td> </td>
<td className="text-muted small">Удалить {s.count} записей → добавить 1</td> <td className="text-muted small">Удалить {s.count} записей → добавить 1</td>
<td className="text-end">
<button
type="button"
className="btn btn-sm btn-warning"
disabled={rowApplying || applying}
onClick={() => {
setApplyConfirmRow(s);
setApplyConfirmOpen(true);
}}
>
{rowApplying ? (
<span className="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true" />
) : null}
Применить
</button>
</td>
</tr> </tr>
); );
})} })}
@@ -542,9 +583,12 @@ export default function FirewallPage() {
<button <button
type="button" type="button"
className="btn btn-warning" className="btn btn-warning"
onClick={() => setApplyConfirmOpen(true)} onClick={() => {
setApplyConfirmRow(null);
setApplyConfirmOpen(true);
}}
> >
Применить на роутере Применить всё на роутере
</button> </button>
</div> </div>
</> </>
@@ -555,19 +599,24 @@ export default function FirewallPage() {
<ConfirmDialog <ConfirmDialog
open={applyConfirmOpen} open={applyConfirmOpen}
title="Применить суммаризацию на роутере?" title={applyConfirmRow ? `Применить для ${applyConfirmRow.subnet}?` : 'Применить суммаризацию на роутере?'}
message={ message={
analysisResult?.suggestions?.length applyConfirmRow
? `Будет удалено ${analysisResult.suggestions.reduce((acc, s) => acc + s.ids.length, 0)} записей и добавлено ${analysisResult.suggestions.length} записей (подсети /24). Продолжить?` ? `Удалить ${applyConfirmRow.count} записей и добавить 1 запись (подсеть ${applyConfirmRow.subnet}). Продолжить?`
: '' : applyPayload
? `Будет удалено ${applyPayload.removeIds.length} записей и добавлено ${applyPayload.addEntries.length} записей (подсети /24). Продолжить?`
: ''
} }
confirmText="Применить" confirmText="Применить"
cancelText="Отмена" cancelText="Отмена"
destructive destructive
size="md" size="md"
loading={applying} loading={applying || !!applyingRow}
onConfirm={handleApplySummary} onConfirm={handleApplySummary}
onCancel={() => setApplyConfirmOpen(false)} onCancel={() => {
setApplyConfirmOpen(false);
setApplyConfirmRow(null);
}}
/> />
</div> </div>
); );