Files
DevOpsLab/app/templates/pages/presets/list.html
Сергей Антропов d4b0d6f848 Исправление синтаксической ошибки в molecule_executor.py и обновление k8s preset'ов
- Исправлена незакрытая скобка в _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
2026-02-16 00:31:09 +03:00

317 lines
14 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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 %}