feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile

- Добавлена колонка 'Тип' во все таблицы истории сборок
- Для push операций отображается registry вместо платформ
- Сохранение пользователя при создании push лога
- Исправлена ошибка с logger в push_docker_image endpoint
- Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
Сергей Антропов
2026-02-15 22:59:02 +03:00
parent 23e1a6037b
commit 1fbf9185a2
232 changed files with 38075 additions and 5 deletions

View File

@@ -0,0 +1,352 @@
{% extends "base.html" %}
{% block title %}Логи сборок - DevOpsLab{% endblock %}
{% block page_title %}Логи сборок{% endblock %}
{% block header_actions %}
<a href="/dockerfiles" class="btn btn-secondary btn-sm">
<i class="fas fa-arrow-left me-2"></i>
Назад к Dockerfile
</a>
{% endblock %}
{% block content %}
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-history me-2"></i>
Все логи сборок
</h5>
<div>
<span class="badge bg-info">Всего: {{ total }}</span>
</div>
</div>
<div class="card-body p-0">
{% if logs %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Dockerfile</th>
<th>Образ</th>
<th>Тип</th>
<th>Платформы</th>
<th>Статус</th>
<th>Начало</th>
<th>Длительность</th>
<th>Пользователь</th>
<th style="min-width: 120px;">Действия</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td class="align-middle">
{% if log.dockerfile_id in dockerfiles_map %}
<a href="/dockerfiles/{{ log.dockerfile_id }}" class="text-decoration-none">
<i class="fas fa-file-code me-1 text-primary"></i>
{{ dockerfiles_map[log.dockerfile_id].name }}
</a>
{% else %}
<span class="text-muted">ID: {{ log.dockerfile_id }}</span>
{% endif %}
</td>
<td class="align-middle">
<code>{{ log.image_name }}{% if log.tag %}:{{ log.tag }}{% endif %}</code>
</td>
<td class="align-middle">
{% if log.extra_data and log.extra_data.get('type') == 'push' %}
<span class="badge bg-info">
<i class="fas fa-upload me-1"></i>Push
</span>
{% else %}
<span class="badge bg-secondary">
<i class="fas fa-hammer me-1"></i>Build
</span>
{% endif %}
</td>
<td class="align-middle">
{% if log.extra_data and log.extra_data.get('type') == 'push' %}
{# Для push показываем registry вместо платформ #}
{% if log.extra_data.get('registry') %}
<span class="badge bg-primary">
<i class="fas fa-server me-1"></i>{{ log.extra_data.get('registry') }}
</span>
{% else %}
<span class="text-muted"></span>
{% endif %}
{% elif log.platforms %}
{% for platform in log.platforms %}
<span class="badge bg-secondary me-1">{{ platform }}</span>
{% endfor %}
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td class="align-middle">
{% if log.status == "success" %}
<span class="badge bg-success">Успешно</span>
{% elif log.status == "failed" %}
<span class="badge bg-danger">Ошибка</span>
{% else %}
<span class="badge bg-warning">Выполняется</span>
{% endif %}
</td>
<td class="align-middle">
{% if log.started_at %}
<small>{{ log.started_at.strftime('%Y-%m-%d %H:%M:%S') }}</small>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td class="align-middle">
{% if log.duration %}
<small>{{ log.duration }} сек</small>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td class="align-middle">
{% if log.user %}
<small>{{ log.user }}</small>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td class="align-middle">
<div class="btn-group btn-group-sm">
<button
type="button"
class="btn btn-outline-primary"
onclick="showLogDetail({{ log.id }})"
title="Просмотр логов"
>
<i class="fas fa-eye"></i>
</button>
<button
type="button"
class="btn btn-outline-danger"
onclick="deleteLog({{ log.id }})"
title="Удалить лог"
>
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Пагинация -->
{% if total_pages > 1 %}
<div class="card-footer">
<nav aria-label="Навигация по страницам">
<ul class="pagination mb-0 justify-content-center">
{% if page > 1 %}
<li class="page-item">
<a class="page-link" href="?page={{ page - 1 }}&per_page={{ per_page }}">Предыдущая</a>
</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 <= 3 or p > total_pages - 3 or (p >= page - 1 and p <= page + 1) %}
<li class="page-item">
<a class="page-link" href="?page={{ p }}&per_page={{ per_page }}">{{ p }}</a>
</li>
{% elif p == 4 or p == total_pages - 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 }}&per_page={{ per_page }}">Следующая</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
{% 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">Логи сборок пока нет</p>
</div>
{% endif %}
</div>
</div>
<!-- Модальное окно для просмотра лога -->
<div class="modal fade" id="logDetailModal" tabindex="-1">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Детали лога сборки</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="log-detail-content" class="log-container">
Загрузка...
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function showLogDetail(logId) {
const modalElement = document.getElementById('logDetailModal');
const modal = new bootstrap.Modal(modalElement);
const content = document.getElementById('log-detail-content');
content.textContent = 'Загрузка...';
// Функция для установки высоты
const setHeight = () => {
const modalBody = document.querySelector('#logDetailModal .modal-body');
const modalHeader = document.querySelector('#logDetailModal .modal-header');
if (modalBody && modalHeader) {
const headerHeight = modalHeader.offsetHeight;
const availableHeight = window.innerHeight - headerHeight;
modalBody.style.height = availableHeight + 'px';
modalBody.style.minHeight = availableHeight + 'px';
modalBody.style.maxHeight = availableHeight + 'px';
content.style.height = availableHeight + 'px';
content.style.minHeight = availableHeight + 'px';
content.style.maxHeight = availableHeight + 'px';
}
};
// Устанавливаем высоту при открытии модального окна
modalElement.addEventListener('shown.bs.modal', function onShown() {
setHeight();
window.addEventListener('resize', setHeight);
modalElement.removeEventListener('shown.bs.modal', onShown);
});
// Убираем обработчик resize при закрытии
modalElement.addEventListener('hidden.bs.modal', function onHidden() {
window.removeEventListener('resize', setHeight);
modalElement.removeEventListener('hidden.bs.modal', onHidden);
});
modal.show();
try {
const response = await fetch(`/api/v1/dockerfiles/build-logs/${logId}`);
const data = await response.json();
if (data.logs) {
content.textContent = data.logs;
} else {
content.textContent = 'Логи не найдены';
}
} catch (error) {
content.textContent = `Ошибка загрузки: ${error.message}`;
}
}
async function deleteLog(logId) {
const confirmed = await showConfirmModal('Вы уверены, что хотите удалить этот лог сборки?');
if (!confirmed) {
return;
}
try {
const response = await fetch(`/api/v1/dockerfiles/build-logs/${logId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
// Удаляем элемент из DOM
const logElement = document.querySelector(`[onclick*="deleteLog(${logId})"]`).closest('tr');
if (logElement) {
logElement.remove();
}
} else {
const data = await response.json();
alert(`Ошибка при удалении: ${data.detail || 'Неизвестная ошибка'}`);
}
} catch (error) {
alert(`Ошибка при удалении: ${error.message}`);
}
}
</script>
<style>
/* Стили для модального окна с логами на весь экран */
#logDetailModal .modal-dialog {
margin: 0 !important;
max-width: 100% !important;
width: 100% !important;
height: 100vh !important;
max-height: 100vh !important;
}
#logDetailModal .modal-content {
height: 100vh !important;
max-height: 100vh !important;
margin: 0 !important;
border: none !important;
border-radius: 0 !important;
display: flex !important;
flex-direction: column !important;
}
#logDetailModal .modal-header {
flex-shrink: 0 !important;
padding: 1rem !important;
height: auto !important;
}
#logDetailModal .modal-body {
flex: 1 1 0 !important;
overflow: hidden !important;
padding: 0 !important;
margin: 0 !important;
width: 100% !important;
min-height: 0 !important;
max-height: none !important;
height: auto !important;
display: flex !important;
flex-direction: column !important;
position: relative !important;
}
#log-detail-content {
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100% !important;
height: 100% !important;
min-height: 0 !important;
max-height: none !important;
box-sizing: border-box !important;
overflow: auto !important;
background: #1e1e1e !important;
color: #d4d4d4 !important;
padding: 1rem !important;
font-family: 'Courier New', monospace !important;
font-size: 0.875rem !important;
white-space: pre !important;
word-wrap: normal !important;
overflow-wrap: normal !important;
}
</style>
{% endblock %}