feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
268
app/templates/pages/presets/detail.html
Normal file
268
app/templates/pages/presets/detail.html
Normal file
@@ -0,0 +1,268 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ preset.name }} - DevOpsLab{% endblock %}
|
||||
{% block page_title %}Preset: {{ preset.name }}{% endblock %}
|
||||
|
||||
{% block header_actions %}
|
||||
<div class="btn-group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-sm"
|
||||
onclick="startPresetTest()"
|
||||
title="Запустить тест preset'а"
|
||||
>
|
||||
<i class="fas fa-play me-2"></i>
|
||||
Запустить
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning btn-sm"
|
||||
onclick="stopPresetTest()"
|
||||
title="Остановить тест"
|
||||
id="stop-btn"
|
||||
style="display: none;"
|
||||
>
|
||||
<i class="fas fa-stop me-2"></i>
|
||||
Остановить
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-info btn-sm"
|
||||
onclick="restartPresetTest()"
|
||||
title="Перезапустить тест"
|
||||
id="restart-btn"
|
||||
style="display: none;"
|
||||
>
|
||||
<i class="fas fa-redo me-2"></i>
|
||||
Перезапустить
|
||||
</button>
|
||||
</div>
|
||||
<a href="/presets/{{ preset.name }}/edit?category={{ preset.category }}" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-edit me-2"></i>
|
||||
Редактировать
|
||||
</a>
|
||||
<a href="/presets" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left me-2"></i>
|
||||
Назад
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-8">
|
||||
<!-- Информация о preset'е -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Информация о preset'е</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<strong>Имя:</strong> {{ preset.name }}<br>
|
||||
<strong>Категория:</strong>
|
||||
<span class="badge bg-{% if preset.category == 'k8s' %}info{% else %}primary{% endif %}">
|
||||
{{ preset.category }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if preset.data %}
|
||||
<strong>Docker сеть:</strong> {{ preset.data.get('docker_network', 'labnet') }}<br>
|
||||
<strong>Хостов:</strong> {{ preset.data.get('hosts', [])|length }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if preset.data and preset.data.get('description') %}
|
||||
<div class="mb-3">
|
||||
<strong>Описание:</strong>
|
||||
<p class="text-muted mb-0">{{ preset.data.description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if preset.data and preset.data.get('hosts') %}
|
||||
<div class="mb-3">
|
||||
<strong>Хосты:</strong>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Имя</th>
|
||||
<th>Семейство</th>
|
||||
<th>Группы</th>
|
||||
<th>Тип</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for host in preset.data.hosts %}
|
||||
<tr>
|
||||
<td>{{ host.name }}</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">{{ host.family }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if host.groups %}
|
||||
{% for group in host.groups %}
|
||||
<span class="badge bg-info me-1">{{ group }}</span>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ host.type or '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if preset.data and preset.data.get('images') %}
|
||||
<div class="mb-3">
|
||||
<strong>Docker образы:</strong>
|
||||
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||
{% for key, value in preset.data.images.items() %}
|
||||
<span class="badge bg-success">
|
||||
<i class="fas fa-cube me-1"></i>
|
||||
{{ key }}: {{ value.split(':')[-1] if ':' in value else value }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- YAML содержимое -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">YAML содержимое</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<pre class="mb-0" style="background: #1e1e1e; color: #d4d4d4; padding: 1rem; margin: 0; overflow-x: auto;"><code>{{ preset.content }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-4">
|
||||
<!-- Логи тестирования -->
|
||||
<div class="card" id="test-logs-card" style="display: none;">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Логи тестирования</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-light"
|
||||
onclick="clearTestLogs()"
|
||||
title="Очистить логи"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="log-container" id="test-logs" style="height: 600px; overflow-y: auto; background: #1e1e1e; color: #d4d4d4; padding: 1rem; font-family: 'Courier New', monospace; font-size: 0.75rem;">
|
||||
<!-- Логи будут добавлены через WebSocket -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let testWebSocket = null;
|
||||
let testRunning = false;
|
||||
|
||||
function startPresetTest() {
|
||||
if (testRunning) {
|
||||
alert('Тест уже запущен');
|
||||
return;
|
||||
}
|
||||
|
||||
// Показываем логи
|
||||
const logsCard = document.getElementById('test-logs-card');
|
||||
const logsContainer = document.getElementById('test-logs');
|
||||
logsCard.style.display = 'block';
|
||||
logsContainer.innerHTML = '';
|
||||
|
||||
// Показываем кнопки управления
|
||||
document.getElementById('stop-btn').style.display = 'inline-block';
|
||||
document.getElementById('restart-btn').style.display = 'inline-block';
|
||||
|
||||
testRunning = true;
|
||||
|
||||
// Создаем WebSocket подключение
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const ws = new WebSocket(`${protocol}//${window.location.host}/ws/preset/test/{{ preset.name }}?category={{ preset.category }}`);
|
||||
testWebSocket = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify({
|
||||
action: 'start',
|
||||
preset_name: '{{ preset.name }}',
|
||||
preset_category: '{{ preset.category }}'
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === 'log' || data.type === 'info') {
|
||||
const logLine = document.createElement('div');
|
||||
logLine.className = `log-line log-${data.level || 'info'}`;
|
||||
logLine.textContent = data.data;
|
||||
logsContainer.appendChild(logLine);
|
||||
logsContainer.scrollTop = logsContainer.scrollHeight;
|
||||
} else if (data.type === 'error') {
|
||||
const errorLine = document.createElement('div');
|
||||
errorLine.className = 'log-line log-error';
|
||||
errorLine.textContent = data.data;
|
||||
logsContainer.appendChild(errorLine);
|
||||
} else if (data.type === 'complete') {
|
||||
const completeLine = document.createElement('div');
|
||||
completeLine.className = 'log-line log-info';
|
||||
completeLine.textContent = data.data || '✅ Тестирование завершено';
|
||||
logsContainer.appendChild(completeLine);
|
||||
testRunning = false;
|
||||
document.getElementById('stop-btn').style.display = 'none';
|
||||
document.getElementById('restart-btn').style.display = 'none';
|
||||
ws.close();
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
const errorLine = document.createElement('div');
|
||||
errorLine.className = 'log-line log-error';
|
||||
errorLine.textContent = `❌ Ошибка подключения: ${error}`;
|
||||
logsContainer.appendChild(errorLine);
|
||||
testRunning = false;
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
testRunning = false;
|
||||
console.log('WebSocket закрыт');
|
||||
};
|
||||
}
|
||||
|
||||
function stopPresetTest() {
|
||||
if (testWebSocket && testWebSocket.readyState === WebSocket.OPEN) {
|
||||
testWebSocket.send(JSON.stringify({ action: 'stop' }));
|
||||
testWebSocket.close();
|
||||
}
|
||||
testRunning = false;
|
||||
document.getElementById('stop-btn').style.display = 'none';
|
||||
document.getElementById('restart-btn').style.display = 'none';
|
||||
}
|
||||
|
||||
function restartPresetTest() {
|
||||
stopPresetTest();
|
||||
setTimeout(() => {
|
||||
startPresetTest();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function clearTestLogs() {
|
||||
document.getElementById('test-logs').innerHTML = '';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user