- Исправлена незакрытая скобка в _build_test_command (строка 745) - Добавлена поддержка k8s preset'ов: выполнение create_k8s_cluster.py перед create.yml - Обновлены образы в k8s preset'ах: заменен недоступный ghcr.io/ansible-community/molecule-ubuntu-systemd:jammy на inecs/ansible-lab:ubuntu22-latest - Обновлены preset'ы в базе данных через SQL - Обновлены файлы: k8s-single.yml, k8s-multi.yml, k8s-istio-full.yml
317 lines
14 KiB
HTML
317 lines
14 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Preset'ы - DevOpsLab{% endblock %}
|
||
{% block page_title %}Preset'ы Molecule{% endblock %}
|
||
|
||
{% block header_actions %}
|
||
<a href="/presets/create" class="btn btn-primary btn-sm">
|
||
<i class="fas fa-plus me-2"></i>
|
||
Создать preset
|
||
</a>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<!-- Поиск и фильтры -->
|
||
<div class="card mb-3">
|
||
<div class="card-body">
|
||
<form method="get" action="/presets" class="row g-3">
|
||
<div class="col-12 col-md-6">
|
||
<input
|
||
type="text"
|
||
name="search"
|
||
value="{{ search }}"
|
||
placeholder="Поиск по имени или описанию..."
|
||
class="form-control"
|
||
>
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<select name="category" class="form-select">
|
||
<option value="">Все категории</option>
|
||
<option value="main" {% if category == 'main' %}selected{% endif %}>Основные</option>
|
||
<option value="k8s" {% if category == 'k8s' %}selected{% endif %}>Kubernetes</option>
|
||
</select>
|
||
</div>
|
||
<div class="col-12 col-md-3">
|
||
<div class="d-flex gap-2">
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="fas fa-search me-1"></i>
|
||
Поиск
|
||
</button>
|
||
{% if search or category %}
|
||
<a href="/presets" class="btn btn-outline-secondary">
|
||
<i class="fas fa-times me-1"></i>
|
||
Сброс
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Таблица preset'ов -->
|
||
<div class="card mb-3">
|
||
<div class="card-header">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<h5 class="mb-0">Список preset'ов</h5>
|
||
<span class="text-muted small">
|
||
Всего: <strong>{{ total }}</strong>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
{% if presets %}
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 25%;">Имя</th>
|
||
<th style="width: 15%;">Категория</th>
|
||
<th style="width: 30%;">Описание</th>
|
||
<th style="width: 15%;">Хосты</th>
|
||
<th style="width: 15%;">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for preset in presets %}
|
||
<tr>
|
||
<td>
|
||
<a href="/presets/{{ preset.name }}{% if preset.category == 'k8s' %}?category=k8s{% endif %}" class="text-decoration-none fw-semibold">
|
||
{{ preset.name }}
|
||
</a>
|
||
</td>
|
||
<td>
|
||
{% if preset.category == 'k8s' %}
|
||
<span class="badge bg-primary">Kubernetes</span>
|
||
{% else %}
|
||
<span class="badge bg-secondary">Основной</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<span class="text-muted small">
|
||
{{ preset.description[:80] if preset.description else "Нет описания" }}{% if preset.description and preset.description|length > 80 %}...{% endif %}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<span class="badge bg-info">{{ preset.hosts_count|default(0) }}</span>
|
||
{% if preset.groups %}
|
||
<div class="mt-1">
|
||
{% for group in preset.groups[:2] %}
|
||
<span class="badge bg-light text-dark small">{{ group }}</span>
|
||
{% endfor %}
|
||
{% if preset.groups|length > 2 %}
|
||
<span class="badge bg-light text-dark small">+{{ preset.groups|length - 2 }}</span>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<div class="btn-group btn-group-sm">
|
||
<a
|
||
href="/presets/{{ preset.name }}/edit{% if preset.category == 'k8s' %}?category=k8s{% endif %}"
|
||
class="btn btn-outline-primary"
|
||
title="Редактировать"
|
||
>
|
||
<i class="fas fa-edit"></i>
|
||
</a>
|
||
<button
|
||
onclick="deletePreset('{{ preset.name }}', '{{ preset.category or 'main' }}', this)"
|
||
class="btn btn-outline-danger"
|
||
title="Удалить"
|
||
>
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
<a
|
||
href="/presets/{{ preset.name }}{% if preset.category == 'k8s' %}?category=k8s{% endif %}"
|
||
class="btn btn-outline-secondary"
|
||
title="Детали"
|
||
>
|
||
<i class="fas fa-info-circle"></i>
|
||
</a>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<div class="text-center py-5">
|
||
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
||
<p class="text-muted mb-3">Preset'ы не найдены</p>
|
||
<a href="/presets/create" class="btn btn-primary">
|
||
<i class="fas fa-plus me-2"></i>
|
||
Создать первый preset
|
||
</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% if presets and total_pages > 1 %}
|
||
<div class="card-footer">
|
||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
|
||
<!-- Пагинация -->
|
||
<nav aria-label="Навигация по страницам">
|
||
<ul class="pagination mb-0">
|
||
{% if page > 1 %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="?page={{ page - 1 }}{% if search %}&search={{ search }}{% endif %}{% if category %}&category={{ category }}{% endif %}">
|
||
<i class="fas fa-chevron-left"></i>
|
||
</a>
|
||
</li>
|
||
{% else %}
|
||
<li class="page-item disabled">
|
||
<span class="page-link">
|
||
<i class="fas fa-chevron-left"></i>
|
||
</span>
|
||
</li>
|
||
{% endif %}
|
||
|
||
{% for p in range(1, total_pages + 1) %}
|
||
{% if p == page %}
|
||
<li class="page-item active">
|
||
<span class="page-link">{{ p }}</span>
|
||
</li>
|
||
{% elif p == 1 or p == total_pages or (p >= page - 2 and p <= page + 2) %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="?page={{ p }}{% if search %}&search={{ search }}{% endif %}{% if category %}&category={{ category }}{% endif %}">{{ p }}</a>
|
||
</li>
|
||
{% elif p == page - 3 or p == page + 3 %}
|
||
<li class="page-item disabled">
|
||
<span class="page-link">...</span>
|
||
</li>
|
||
{% endif %}
|
||
{% endfor %}
|
||
|
||
{% if page < total_pages %}
|
||
<li class="page-item">
|
||
<a class="page-link" href="?page={{ page + 1 }}{% if search %}&search={{ search }}{% endif %}{% if category %}&category={{ category }}{% endif %}">
|
||
<i class="fas fa-chevron-right"></i>
|
||
</a>
|
||
</li>
|
||
{% else %}
|
||
<li class="page-item disabled">
|
||
<span class="page-link">
|
||
<i class="fas fa-chevron-right"></i>
|
||
</span>
|
||
</li>
|
||
{% endif %}
|
||
</ul>
|
||
</nav>
|
||
|
||
<!-- Выбор количества на странице -->
|
||
<div class="d-flex align-items-center gap-2">
|
||
<span class="text-muted small">На странице:</span>
|
||
<select
|
||
class="form-select form-select-sm pagination-per-page-select"
|
||
style="width: auto;"
|
||
onchange="window.location.href = '?page=1&per_page=' + this.value + '{% if search %}&search={{ search }}{% endif %}{% if category %}&category={{ category }}{% endif %}'"
|
||
>
|
||
<option value="10" {% if per_page == 10 %}selected{% endif %}>10</option>
|
||
<option value="25" {% if per_page == 25 %}selected{% endif %}>25</option>
|
||
<option value="50" {% if per_page == 50 %}selected{% endif %}>50</option>
|
||
<option value="100" {% if per_page == 100 %}selected{% endif %}>100</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Информация о странице -->
|
||
<div class="text-muted small">
|
||
Показано {{ ((page - 1) * per_page) + 1 }} - {{ [page * per_page, total]|min }} из {{ total }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
async function deletePreset(presetName, category, button) {
|
||
// Показываем модальное окно подтверждения
|
||
const confirmed = await showConfirmModal(
|
||
`Вы уверены, что хотите удалить preset '${presetName}'?`,
|
||
'Подтверждение удаления'
|
||
);
|
||
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
|
||
// Отключаем кнопку во время запроса
|
||
button.disabled = true;
|
||
const originalHTML = button.innerHTML;
|
||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||
|
||
try {
|
||
const response = await fetch(`/api/v1/presets/${presetName}?category=${category}`, {
|
||
method: 'DELETE',
|
||
credentials: 'include',
|
||
headers: {
|
||
'Accept': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
// Показываем модальное окно с успешным сообщением
|
||
if (window.showMessageModal) {
|
||
window.showMessageModal(
|
||
data.message || `Preset '${presetName}' успешно удален`,
|
||
'success',
|
||
'Успешно',
|
||
function() {
|
||
// После закрытия модального окна удаляем строку из таблицы
|
||
const row = button.closest('tr');
|
||
if (row) {
|
||
row.remove();
|
||
// Обновляем счетчик, если нужно
|
||
const totalSpan = document.querySelector('.text-muted.small strong');
|
||
if (totalSpan) {
|
||
const currentTotal = parseInt(totalSpan.textContent) || 0;
|
||
totalSpan.textContent = Math.max(0, currentTotal - 1);
|
||
}
|
||
}
|
||
}
|
||
);
|
||
} else {
|
||
// Если функция недоступна, просто удаляем строку
|
||
const row = button.closest('tr');
|
||
if (row) {
|
||
row.remove();
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// Ошибка - показываем в модальном окне
|
||
try {
|
||
const errorData = await response.json();
|
||
const errorMessage = errorData.detail || errorData.message || 'Ошибка при удалении preset';
|
||
if (window.showMessageModal) {
|
||
window.showMessageModal(errorMessage, 'error');
|
||
} else {
|
||
alert(errorMessage);
|
||
}
|
||
} catch (e) {
|
||
if (window.showMessageModal) {
|
||
window.showMessageModal('Ошибка при удалении preset', 'error');
|
||
} else {
|
||
alert('Ошибка при удалении preset');
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка при удалении preset:', error);
|
||
if (window.showMessageModal) {
|
||
window.showMessageModal('Ошибка при удалении preset', 'error');
|
||
} else {
|
||
alert('Ошибка при удалении preset');
|
||
}
|
||
} finally {
|
||
// Восстанавливаем кнопку
|
||
button.disabled = false;
|
||
button.innerHTML = originalHTML;
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %}
|