549 lines
30 KiB
HTML
549 lines
30 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Firewall Rules Builder</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||
<link href="/static/style.css" rel="stylesheet">
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Navbar -->
|
||
<nav class="navbar navbar-dark bg-dark px-3 py-2">
|
||
<span class="navbar-brand fw-bold"><i class="bi bi-shield-lock-fill me-2 text-info"></i>Firewall Rules Builder</span>
|
||
<div class="d-flex gap-2">
|
||
<button class="btn btn-sm btn-outline-warning" onclick="App.importSettings()">
|
||
<i class="bi bi-upload me-1"></i>Импорт fw_settings.py
|
||
</button>
|
||
<a class="btn btn-sm btn-outline-success" href="/api/export" download="fw_settings.py">
|
||
<i class="bi bi-download me-1"></i>Экспорт fw_settings.py
|
||
</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Tabs -->
|
||
<div class="container-fluid mt-3">
|
||
<ul class="nav nav-tabs" id="mainTabs">
|
||
<li class="nav-item"><a class="nav-link active" data-bs-toggle="tab" href="#tab-objects"><i class="bi bi-hdd-network me-1"></i>Объекты</a></li>
|
||
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-groups"><i class="bi bi-collection me-1"></i>Группы объектов</a></li>
|
||
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-services"><i class="bi bi-gear me-1"></i>Сервисы</a></li>
|
||
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-svcgroups"><i class="bi bi-layers me-1"></i>Группы сервисов</a></li>
|
||
<li class="nav-item"><a class="nav-link" data-bs-toggle="tab" href="#tab-rules"><i class="bi bi-list-check me-1"></i>Правила</a></li>
|
||
</ul>
|
||
|
||
<div class="tab-content mt-3">
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
||
<!-- TAB: Объекты -->
|
||
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
||
<div class="tab-pane fade show active" id="tab-objects">
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<h5 class="mb-0">Объекты (хосты и сети)</h5>
|
||
<button class="btn btn-primary btn-sm" onclick="Objects.openModal()">
|
||
<i class="bi bi-plus-lg me-1"></i>Добавить объект
|
||
</button>
|
||
</div>
|
||
<div class="mb-2">
|
||
<input type="text" class="form-control form-control-sm w-auto d-inline-block" id="obj-search" placeholder="Поиск..." oninput="Objects.filter(this.value)">
|
||
<select class="form-select form-select-sm w-auto d-inline-block ms-2" id="obj-type-filter" onchange="Objects.filter()">
|
||
<option value="">Все типы</option>
|
||
<option value="host">host</option>
|
||
<option value="network">network</option>
|
||
</select>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-hover table-bordered align-middle" id="objects-table">
|
||
<thead class="table-dark">
|
||
<tr>
|
||
<th>Ключ</th><th>Тип</th><th>IP</th><th>Префикс</th><th>Шлюз</th><th>Домен</th><th>Описание</th><th>Affinity</th><th style="width:90px"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="objects-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
||
<!-- TAB: Группы объектов -->
|
||
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
||
<div class="tab-pane fade" id="tab-groups">
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<h5 class="mb-0">Группы объектов</h5>
|
||
<button class="btn btn-primary btn-sm" onclick="Groups.openModal()">
|
||
<i class="bi bi-plus-lg me-1"></i>Добавить группу
|
||
</button>
|
||
</div>
|
||
<div class="mb-2">
|
||
<input type="text" class="form-control form-control-sm w-auto d-inline-block" id="grp-search" placeholder="Поиск..." oninput="Groups.filter(this.value)">
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-hover table-bordered align-middle" id="groups-table">
|
||
<thead class="table-dark">
|
||
<tr><th>Ключ</th><th>Имя</th><th>Элементы</th><th style="width:90px"></th></tr>
|
||
</thead>
|
||
<tbody id="groups-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
||
<!-- TAB: Сервисы -->
|
||
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
||
<div class="tab-pane fade" id="tab-services">
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<h5 class="mb-0">Сервисы</h5>
|
||
<button class="btn btn-primary btn-sm" onclick="Services.openModal()">
|
||
<i class="bi bi-plus-lg me-1"></i>Добавить сервис
|
||
</button>
|
||
</div>
|
||
<div class="mb-2">
|
||
<input type="text" class="form-control form-control-sm w-auto d-inline-block" id="svc-search" placeholder="Поиск..." oninput="Services.filter(this.value)">
|
||
<select class="form-select form-select-sm w-auto d-inline-block ms-2" id="svc-proto-filter" onchange="Services.filter()">
|
||
<option value="">Все протоколы</option>
|
||
<option value="tcp">tcp</option>
|
||
<option value="udp">udp</option>
|
||
<option value="icmp-request">icmp</option>
|
||
</select>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-hover table-bordered align-middle">
|
||
<thead class="table-dark">
|
||
<tr><th>Ключ</th><th>Имя</th><th>Протокол</th><th>Порт источника</th><th>Порт назначения</th><th style="width:90px"></th></tr>
|
||
</thead>
|
||
<tbody id="services-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
||
<!-- TAB: Группы сервисов -->
|
||
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
||
<div class="tab-pane fade" id="tab-svcgroups">
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<h5 class="mb-0">Группы сервисов</h5>
|
||
<button class="btn btn-primary btn-sm" onclick="SvcGroups.openModal()">
|
||
<i class="bi bi-plus-lg me-1"></i>Добавить группу сервисов
|
||
</button>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-hover table-bordered align-middle">
|
||
<thead class="table-dark">
|
||
<tr><th>Ключ</th><th>Имя</th><th>Сервисы</th><th style="width:90px"></th></tr>
|
||
</thead>
|
||
<tbody id="svcgroups-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
||
<!-- TAB: Правила -->
|
||
<!-- ═══════════════════════════════════════════════════════════════════ -->
|
||
<div class="tab-pane fade" id="tab-rules">
|
||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||
<h5 class="mb-0">Правила Firewall</h5>
|
||
<div class="d-flex gap-2">
|
||
<button class="btn btn-secondary btn-sm" onclick="Rules.openSpanModal()">
|
||
<i class="bi bi-dash-lg me-1"></i>Добавить разделитель
|
||
</button>
|
||
<button class="btn btn-primary btn-sm" onclick="Rules.openModal()">
|
||
<i class="bi bi-plus-lg me-1"></i>Добавить правило
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-hover table-bordered align-middle" id="rules-table">
|
||
<thead class="table-dark">
|
||
<tr>
|
||
<th style="width:40px">#</th>
|
||
<th>Порядок</th>
|
||
<th>Имя</th>
|
||
<th>Описание</th>
|
||
<th>Источник</th>
|
||
<th>Назначение</th>
|
||
<th>Сервисы</th>
|
||
<th>Действие</th>
|
||
<th>Лог</th>
|
||
<th>IDP</th>
|
||
<th>Affinity</th>
|
||
<th style="width:100px"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="rules-tbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /tab-content -->
|
||
</div><!-- /container -->
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
|
||
<!-- MODAL: Объект -->
|
||
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
|
||
<div class="modal fade" id="modal-object" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header bg-dark text-white">
|
||
<h5 class="modal-title" id="modal-object-title">Объект</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="form-object">
|
||
<input type="hidden" id="obj-edit-key">
|
||
<div class="row g-2">
|
||
<div class="col-md-4">
|
||
<label class="form-label">Ключ <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="obj-key" required placeholder="cr">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">Hostname</label>
|
||
<input type="text" class="form-control" id="obj-hostname" placeholder="cr">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">Тип <span class="text-danger">*</span></label>
|
||
<select class="form-select" id="obj-type">
|
||
<option value="host">host</option>
|
||
<option value="network">network</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">IP-адрес</label>
|
||
<input type="text" class="form-control" id="obj-ip" placeholder="172.19.20.2">
|
||
</div>
|
||
<div class="col-md-2">
|
||
<label class="form-label">Префикс</label>
|
||
<input type="text" class="form-control" id="obj-prefix" placeholder="24">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">Шлюз</label>
|
||
<input type="text" class="form-control" id="obj-gw" placeholder="172.19.20.1">
|
||
</div>
|
||
<div class="col-md-2">
|
||
<label class="form-label">Домен</label>
|
||
<input type="text" class="form-control" id="obj-domain" placeholder="avndr.ru">
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">Описание</label>
|
||
<input type="text" class="form-control" id="obj-description" placeholder="Описание объекта">
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">Affinity (через запятую)</label>
|
||
<input type="text" class="form-control" id="obj-affinity" placeholder="fw_cr, fw_cr_ca">
|
||
</div>
|
||
</div>
|
||
<div id="obj-error" class="alert alert-danger mt-2 d-none"></div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button class="btn btn-primary" onclick="Objects.save()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
|
||
<!-- MODAL: Группа объектов -->
|
||
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
|
||
<div class="modal fade" id="modal-group" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header bg-dark text-white">
|
||
<h5 class="modal-title" id="modal-group-title">Группа объектов</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="form-group">
|
||
<input type="hidden" id="grp-edit-key">
|
||
<div class="row g-2 mb-2">
|
||
<div class="col-md-5">
|
||
<label class="form-label">Ключ <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="grp-key" required placeholder="set_dr">
|
||
</div>
|
||
<div class="col-md-7">
|
||
<label class="form-label">Имя</label>
|
||
<input type="text" class="form-control" id="grp-name" placeholder="set_dr">
|
||
</div>
|
||
</div>
|
||
<label class="form-label">Элементы группы</label>
|
||
<div class="row g-2 mb-2">
|
||
<div class="col-md-8">
|
||
<input type="text" class="form-control" id="grp-item-search" placeholder="Поиск объекта..." oninput="Groups.filterItems(this.value)">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<select class="form-select" id="grp-item-type-filter" onchange="Groups.filterItems()">
|
||
<option value="">Все типы</option>
|
||
<option value="host">host</option>
|
||
<option value="network">network</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="row g-2">
|
||
<div class="col-md-6">
|
||
<div class="fw-bold small mb-1 text-muted">Доступные объекты</div>
|
||
<div class="border rounded p-1" style="height:260px;overflow-y:auto;" id="grp-available-list"></div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="fw-bold small mb-1 text-muted">Выбранные элементы <span class="badge bg-primary" id="grp-selected-count">0</span></div>
|
||
<div class="border rounded p-1" style="height:260px;overflow-y:auto;" id="grp-selected-list"></div>
|
||
</div>
|
||
</div>
|
||
<div id="grp-error" class="alert alert-danger mt-2 d-none"></div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button class="btn btn-primary" onclick="Groups.save()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
|
||
<!-- MODAL: Сервис -->
|
||
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
|
||
<div class="modal fade" id="modal-service" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header bg-dark text-white">
|
||
<h5 class="modal-title" id="modal-service-title">Сервис</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="form-service">
|
||
<input type="hidden" id="svc-edit-key">
|
||
<div class="row g-2">
|
||
<div class="col-md-6">
|
||
<label class="form-label">Ключ <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="svc-key" required placeholder="ssh">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">Имя</label>
|
||
<input type="text" class="form-control" id="svc-name" placeholder="ssh-22-tcp">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">Протокол</label>
|
||
<select class="form-select" id="svc-proto">
|
||
<option value="tcp">tcp</option>
|
||
<option value="udp">udp</option>
|
||
<option value="icmp-request">icmp-request</option>
|
||
<option value="any">any</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">Порт источника</label>
|
||
<input type="text" class="form-control" id="svc-sport" placeholder="any">
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label">Порт назначения</label>
|
||
<input type="text" class="form-control" id="svc-dport" placeholder="22">
|
||
</div>
|
||
</div>
|
||
<div id="svc-error" class="alert alert-danger mt-2 d-none"></div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button class="btn btn-primary" onclick="Services.save()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
|
||
<!-- MODAL: Группа сервисов -->
|
||
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
|
||
<div class="modal fade" id="modal-svcgroup" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header bg-dark text-white">
|
||
<h5 class="modal-title" id="modal-svcgroup-title">Группа сервисов</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="form-svcgroup">
|
||
<input type="hidden" id="sg-edit-key">
|
||
<div class="row g-2 mb-2">
|
||
<div class="col-md-5">
|
||
<label class="form-label">Ключ <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="sg-key" required placeholder="sg_dns">
|
||
</div>
|
||
<div class="col-md-7">
|
||
<label class="form-label">Имя</label>
|
||
<input type="text" class="form-control" id="sg-name" placeholder="sg_dns">
|
||
</div>
|
||
</div>
|
||
<label class="form-label">Сервисы в группе</label>
|
||
<div class="mb-2">
|
||
<input type="text" class="form-control" id="sg-svc-search" placeholder="Поиск сервиса..." oninput="SvcGroups.filterItems(this.value)">
|
||
</div>
|
||
<div class="row g-2">
|
||
<div class="col-md-6">
|
||
<div class="fw-bold small mb-1 text-muted">Доступные сервисы</div>
|
||
<div class="border rounded p-1" style="height:240px;overflow-y:auto;" id="sg-available-list"></div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="fw-bold small mb-1 text-muted">Выбранные сервисы <span class="badge bg-primary" id="sg-selected-count">0</span></div>
|
||
<div class="border rounded p-1" style="height:240px;overflow-y:auto;" id="sg-selected-list"></div>
|
||
</div>
|
||
</div>
|
||
<div id="sg-error" class="alert alert-danger mt-2 d-none"></div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button class="btn btn-primary" onclick="SvcGroups.save()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
|
||
<!-- MODAL: Правило -->
|
||
<!-- ═══════════════════════════════════════════════════════════════════════════ -->
|
||
<div class="modal fade" id="modal-rule" tabindex="-1">
|
||
<div class="modal-dialog modal-xl">
|
||
<div class="modal-content">
|
||
<div class="modal-header bg-dark text-white">
|
||
<h5 class="modal-title" id="modal-rule-title">Правило</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="form-rule">
|
||
<input type="hidden" id="rule-edit-idx">
|
||
<div class="row g-2 mb-3">
|
||
<div class="col-md-5">
|
||
<label class="form-label">Имя правила <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="rule-name" required placeholder="to_dns">
|
||
</div>
|
||
<div class="col-md-2">
|
||
<label class="form-label">Порядок</label>
|
||
<input type="number" class="form-control" id="rule-order" placeholder="1000">
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label class="form-label">Действие</label>
|
||
<select class="form-select" id="rule-action">
|
||
<option value="allow">allow</option>
|
||
<option value="deny">deny</option>
|
||
<option value="drop">drop</option>
|
||
<option value="reject">reject</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-1">
|
||
<label class="form-label">Лог</label>
|
||
<select class="form-select" id="rule-log">
|
||
<option value="false">false</option>
|
||
<option value="true">true</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-md-1">
|
||
<label class="form-label">IDP</label>
|
||
<select class="form-select" id="rule-idp">
|
||
<option value="false">false</option>
|
||
<option value="true">true</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">Описание</label>
|
||
<input type="text" class="form-control" id="rule-description" placeholder="Описание правила">
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">Affinity (через запятую)</label>
|
||
<input type="text" class="form-control" id="rule-affinity" placeholder="fw_cr, fw_cr_ca">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Источник / Назначение / Сервисы -->
|
||
<div class="row g-3">
|
||
<!-- Источник -->
|
||
<div class="col-md-4">
|
||
<div class="card h-100">
|
||
<div class="card-header py-1 fw-bold">Источник (src)</div>
|
||
<div class="card-body p-2">
|
||
<input type="text" class="form-control form-control-sm mb-1" id="rule-src-search" placeholder="Поиск..." oninput="Rules.filterSrc(this.value)">
|
||
<div class="fw-bold small text-muted mb-1">Доступные</div>
|
||
<div class="border rounded p-1 mb-2" style="height:180px;overflow-y:auto;" id="rule-src-available"></div>
|
||
<div class="fw-bold small text-muted mb-1">Выбранные <span class="badge bg-primary" id="rule-src-count">0</span></div>
|
||
<div class="border rounded p-1" style="height:120px;overflow-y:auto;" id="rule-src-selected"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Назначение -->
|
||
<div class="col-md-4">
|
||
<div class="card h-100">
|
||
<div class="card-header py-1 fw-bold">Назначение (dst)</div>
|
||
<div class="card-body p-2">
|
||
<input type="text" class="form-control form-control-sm mb-1" id="rule-dst-search" placeholder="Поиск..." oninput="Rules.filterDst(this.value)">
|
||
<div class="fw-bold small text-muted mb-1">Доступные</div>
|
||
<div class="border rounded p-1 mb-2" style="height:180px;overflow-y:auto;" id="rule-dst-available"></div>
|
||
<div class="fw-bold small text-muted mb-1">Выбранные <span class="badge bg-primary" id="rule-dst-count">0</span></div>
|
||
<div class="border rounded p-1" style="height:120px;overflow-y:auto;" id="rule-dst-selected"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Сервисы -->
|
||
<div class="col-md-4">
|
||
<div class="card h-100">
|
||
<div class="card-header py-1 fw-bold">Сервисы</div>
|
||
<div class="card-body p-2">
|
||
<input type="text" class="form-control form-control-sm mb-1" id="rule-svc-search" placeholder="Поиск..." oninput="Rules.filterSvc(this.value)">
|
||
<div class="fw-bold small text-muted mb-1">Доступные</div>
|
||
<div class="border rounded p-1 mb-2" style="height:180px;overflow-y:auto;" id="rule-svc-available"></div>
|
||
<div class="fw-bold small text-muted mb-1">Выбранные <span class="badge bg-primary" id="rule-svc-count">0</span></div>
|
||
<div class="border rounded p-1" style="height:120px;overflow-y:auto;" id="rule-svc-selected"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="rule-error" class="alert alert-danger mt-2 d-none"></div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button class="btn btn-primary" onclick="Rules.save()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODAL: Разделитель (span) -->
|
||
<div class="modal fade" id="modal-span" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header bg-secondary text-white">
|
||
<h5 class="modal-title">Разделитель (span)</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<input type="hidden" id="span-edit-idx">
|
||
<div class="mb-2">
|
||
<label class="form-label">Название секции</label>
|
||
<input type="text" class="form-control" id="span-name" placeholder="Инфраструктурные правила">
|
||
</div>
|
||
<div class="mb-2">
|
||
<label class="form-label">Порядок</label>
|
||
<input type="number" class="form-control" id="span-order" placeholder="1000">
|
||
</div>
|
||
<div class="mb-2">
|
||
<label class="form-label">Affinity (через запятую)</label>
|
||
<input type="text" class="form-control" id="span-affinity" placeholder="fw_cr">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button class="btn btn-primary" onclick="Rules.saveSpan()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast уведомления -->
|
||
<div class="toast-container position-fixed bottom-0 end-0 p-3" id="toast-container"></div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script src="/static/app.js"></script>
|
||
</body>
|
||
</html>
|