feat(MikrotikConfig): add API endpoint for applying MikroTik configuration; update frontend to support configuration application with dry-run option and modal interface
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m56s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m56s
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
IconLayoutGrid,
|
||||
IconList,
|
||||
IconCode,
|
||||
IconPlug,
|
||||
IconArrowsRightLeft,
|
||||
IconLock,
|
||||
IconChevronDown,
|
||||
@@ -231,6 +232,11 @@ function NetworkConfigManager() {
|
||||
// === MikroTik Code Generation ===
|
||||
const [mikrotikCodeModalOpen, setMikrotikCodeModalOpen] = useState(false);
|
||||
const [generatedMikrotikCode, setGeneratedMikrotikCode] = useState([]);
|
||||
const [applyModalOpen, setApplyModalOpen] = useState(false);
|
||||
const [applyServerId, setApplyServerId] = useState(null);
|
||||
const [applyDryRun, setApplyDryRun] = useState(true);
|
||||
const [applyLoading, setApplyLoading] = useState(false);
|
||||
const [applyResult, setApplyResult] = useState(null);
|
||||
|
||||
// === Глобальные настройки PTR зоны ===
|
||||
const [globalPtrZoneReplaceFrom, setGlobalPtrZoneReplaceFrom] = useState('');
|
||||
@@ -2215,6 +2221,40 @@ function NetworkConfigManager() {
|
||||
}
|
||||
};
|
||||
|
||||
// === Применить конфигурацию по API MikroTik ===
|
||||
const handleApplyMikrotikViaApi = async (dryRunOverride = null) => {
|
||||
if (!applyServerId) return;
|
||||
const useDryRun = dryRunOverride !== null ? dryRunOverride : applyDryRun;
|
||||
setApplyLoading(true);
|
||||
if (dryRunOverride !== null) setApplyResult(null);
|
||||
try {
|
||||
const { data } = await api.post('/mikrotik/apply', {
|
||||
serverId: applyServerId,
|
||||
type: 'all',
|
||||
dryRun: useDryRun,
|
||||
});
|
||||
setApplyResult(data);
|
||||
if (data.ok && !useDryRun) {
|
||||
notify.success(`Применено: создано ${data.summary?.created || 0}, обновлено ${data.summary?.updated || 0}, пропущено ${data.summary?.skipped || 0}`);
|
||||
}
|
||||
} catch (error) {
|
||||
setApplyResult({
|
||||
ok: false,
|
||||
error: error.response?.data?.error || error.message || 'Ошибка применения',
|
||||
});
|
||||
notify.error('Не удалось применить конфигурацию по API');
|
||||
} finally {
|
||||
setApplyLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openApplyModal = (serverId) => {
|
||||
setApplyServerId(serverId);
|
||||
setApplyResult(null);
|
||||
setApplyDryRun(true);
|
||||
setApplyModalOpen(true);
|
||||
};
|
||||
|
||||
// === Генерация кода MikroTik только для одного интерфейса ===
|
||||
const handleGenerateMikrotikCodeForInterface = async (iface) => {
|
||||
if (!iface) {
|
||||
@@ -2641,18 +2681,34 @@ function NetworkConfigManager() {
|
||||
{items.length} {type === 'gateway' ? (items.length === 1 ? 'gateway' : 'gateways') : (items.length === 1 ? 'интерфейс' : 'интерфейсов')}
|
||||
</span>
|
||||
{!isUnassigned && items.length > 0 && (
|
||||
<button
|
||||
className="btn btn-primary btn-sm flex-shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleGenerateMikrotikCodeForServer(serverId, type);
|
||||
}}
|
||||
title={`Получить код MikroTik для всех ${type === 'gateway' ? 'gateways' : 'интерфейсов'} этого сервера`}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
<IconCode size={16} className="me-1" />
|
||||
Код для MikroTik
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
className="btn btn-primary btn-sm flex-shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleGenerateMikrotikCodeForServer(serverId, type);
|
||||
}}
|
||||
title={`Получить код MikroTik для всех ${type === 'gateway' ? 'gateways' : 'интерфейсов'} этого сервера`}
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
<IconCode size={16} className="me-1" />
|
||||
Код для MikroTik
|
||||
</button>
|
||||
{server?.type === 'jumphost' && (
|
||||
<button
|
||||
className="btn btn-outline-success btn-sm flex-shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openApplyModal(serverId);
|
||||
}}
|
||||
title="Применить конфигурацию на MikroTik по API"
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
<IconPlug size={16} className="me-1" />
|
||||
Применить по API
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -5230,6 +5286,119 @@ function NetworkConfigManager() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Apply via API Modal */}
|
||||
{applyModalOpen && (
|
||||
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||
<div className="modal-dialog modal-lg">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title d-flex align-items-center gap-2">
|
||||
<IconPlug size={20} />
|
||||
Применить конфигурацию на MikroTik по API
|
||||
</h5>
|
||||
<button type="button" className="btn-close" onClick={() => setApplyModalOpen(false)}></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
{!applyResult ? (
|
||||
<>
|
||||
<div className="form-check mb-3">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
id="applyDryRun"
|
||||
checked={applyDryRun}
|
||||
onChange={(e) => setApplyDryRun(e.target.checked)}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="applyDryRun">
|
||||
Dry-run — только показать план, не выполнять изменения
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-muted small mb-0">
|
||||
Будет применена конфигурация интерфейсов (GRE, IP) и рекурсивных маршрутов для выбранного сервера.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{applyResult.ok ? (
|
||||
<>
|
||||
<div className={`alert alert-${applyResult.dryRun ? 'info' : 'success'} mb-3`}>
|
||||
{applyResult.dryRun ? (
|
||||
<>Режим dry-run: показан план изменений (ничего не применено)</>
|
||||
) : (
|
||||
<>Конфигурация успешно применена</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<strong>Итого:</strong>{' '}
|
||||
создано: {applyResult.summary?.created ?? 0}, обновлено: {applyResult.summary?.updated ?? 0}, пропущено: {applyResult.summary?.skipped ?? 0}
|
||||
{applyResult.summary?.errors?.length > 0 && (
|
||||
<span className="text-danger ms-2">, ошибок: {applyResult.summary.errors.length}</span>
|
||||
)}
|
||||
</div>
|
||||
{applyResult.results?.map((block, bi) => (
|
||||
<div key={bi} className="border rounded p-2 mb-2">
|
||||
<div className="fw-medium mb-2">{block.serverName} — {block.blockType}</div>
|
||||
<ul className="list-unstyled mb-0 small">
|
||||
{block.results?.slice(0, 15).map((r, ri) => (
|
||||
<li key={ri} className="text-muted">
|
||||
{r.status === 'created' || r.status === 'would_create' ? '✓ ' : ''}
|
||||
{r.status === 'updated' || r.status === 'would_update' ? '↻ ' : ''}
|
||||
{r.status === 'skip' ? '○ ' : ''}
|
||||
{r.status === 'error' ? '✗ ' : ''}
|
||||
{r.details || r.error || `${r.path} ${r.action}`}
|
||||
</li>
|
||||
))}
|
||||
{(block.results?.length || 0) > 15 && (
|
||||
<li className="text-muted">... и ещё {block.results.length - 15}</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="alert alert-danger mb-0">{applyResult.error}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
{!applyResult ? (
|
||||
<>
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setApplyModalOpen(false)}>
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success"
|
||||
onClick={handleApplyMikrotikViaApi}
|
||||
disabled={applyLoading}
|
||||
>
|
||||
{applyLoading ? 'Выполнение...' : (applyDryRun ? 'Показать план' : 'Применить')}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{applyResult.dryRun && applyResult.ok && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-success me-2"
|
||||
onClick={() => handleApplyMikrotikViaApi(false)}
|
||||
disabled={applyLoading}
|
||||
>
|
||||
Применить по-настоящему
|
||||
</button>
|
||||
)}
|
||||
<button type="button" className="btn btn-primary" onClick={() => setApplyModalOpen(false)}>
|
||||
Закрыть
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gateway Template Modal */}
|
||||
<FormModal
|
||||
show={gatewayTemplateModalOpen}
|
||||
|
||||
Reference in New Issue
Block a user