- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
353 lines
14 KiB
HTML
353 lines
14 KiB
HTML
{% 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 %}
|