Files
DevOpsLab/app/templates/pages/roles/edit.html
Сергей Антропов 1fbf9185a2 feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок
- Для push операций отображается registry вместо платформ
- Сохранение пользователя при создании push лога
- Исправлена ошибка с logger в push_docker_image endpoint
- Улучшено отображение истории сборок с визуальными индикаторами
2026-02-15 22:59:02 +03:00

457 lines
18 KiB
HTML
Raw 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 %}Редактировать {{ role.name }} - DevOpsLab{% endblock %}
{% block page_title %}Редактирование роли: {{ role.name }}{% endblock %}
{% block header_actions %}
<a href="/roles/{{ role.name }}" class="btn btn-secondary btn-sm">
<i class="fas fa-arrow-left me-2"></i>
Назад
</a>
{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button
class="nav-link active"
id="tasks-tab"
data-bs-toggle="tab"
data-bs-target="#tasks"
type="button"
role="tab"
>
<i class="fas fa-tasks me-2"></i>
Tasks
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="handlers-tab"
data-bs-toggle="tab"
data-bs-target="#handlers"
type="button"
role="tab"
>
<i class="fas fa-cogs me-2"></i>
Handlers
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="defaults-tab"
data-bs-toggle="tab"
data-bs-target="#defaults"
type="button"
role="tab"
>
<i class="fas fa-sliders-h me-2"></i>
Defaults
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="vars-tab"
data-bs-toggle="tab"
data-bs-target="#vars"
type="button"
role="tab"
>
<i class="fas fa-key me-2"></i>
Vars
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="meta-tab"
data-bs-toggle="tab"
data-bs-target="#meta"
type="button"
role="tab"
>
<i class="fas fa-info-circle me-2"></i>
Meta
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="readme-tab"
data-bs-toggle="tab"
data-bs-target="#readme"
type="button"
role="tab"
>
<i class="fas fa-book me-2"></i>
README
</button>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content" id="role-edit-tabs">
<!-- Tasks -->
<div class="tab-pane fade show active" id="tasks" role="tabpanel">
<form
hx-post="/api/v1/roles/{{ role.name }}/update"
hx-target="#tasks-result"
hx-swap="innerHTML"
class="mb-3"
>
<input type="hidden" name="file_type" value="tasks">
<div class="mb-3">
<label class="form-label">tasks/main.yml</label>
<textarea
name="content"
rows="20"
class="form-control font-monospace"
required
>{{ files_content.get('tasks', '') }}</textarea>
</div>
<div id="tasks-result"></div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
Сохранить
</button>
</form>
</div>
<!-- Handlers -->
<div class="tab-pane fade" id="handlers" role="tabpanel">
<form
hx-post="/api/v1/roles/{{ role.name }}/update"
hx-target="#handlers-result"
hx-swap="innerHTML"
class="mb-3"
>
<input type="hidden" name="file_type" value="handlers">
<div class="mb-3">
<label class="form-label">handlers/main.yml</label>
<textarea
name="content"
id="handlers-content"
rows="20"
class="form-control font-monospace"
required
>{{ files_content.get('handlers', '') }}</textarea>
</div>
<div id="handlers-result"></div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
Сохранить
</button>
</form>
</div>
<!-- Defaults -->
<div class="tab-pane fade" id="defaults" role="tabpanel">
<form
hx-post="/api/v1/roles/{{ role.name }}/update"
hx-target="#defaults-result"
hx-swap="innerHTML"
class="mb-3"
>
<input type="hidden" name="file_type" value="defaults">
<div class="mb-3">
<label class="form-label">defaults/main.yml</label>
<textarea
name="content"
id="defaults-content"
rows="20"
class="form-control font-monospace"
required
>{{ files_content.get('defaults', '') }}</textarea>
</div>
<div id="defaults-result"></div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
Сохранить
</button>
</form>
</div>
<!-- Vars -->
<div class="tab-pane fade" id="vars" role="tabpanel">
<div class="alert alert-warning mb-3">
<i class="fas fa-lock me-2"></i>
<strong>Внимание:</strong> Если файл зашифрован через Ansible Vault, используйте кнопку "Расшифровать" для редактирования.
</div>
<form
hx-post="/api/v1/roles/{{ role.name }}/update"
hx-target="#vars-result"
hx-swap="innerHTML"
class="mb-3"
>
<input type="hidden" name="file_type" value="vars">
<div class="mb-3">
<label class="form-label">vars/main.yml</label>
<div class="d-flex gap-2 mb-2">
<button
type="button"
class="btn btn-sm btn-outline-secondary"
onclick="checkVaultEncryption('vars')"
>
<i class="fas fa-search me-2"></i>
Проверить Vault
</button>
<button
type="button"
class="btn btn-sm btn-outline-primary"
onclick="decryptVault('vars')"
id="decrypt-vars-btn"
style="display: none;"
>
<i class="fas fa-unlock me-2"></i>
Расшифровать
</button>
<button
type="button"
class="btn btn-sm btn-outline-success"
onclick="encryptVault('vars')"
id="encrypt-vars-btn"
style="display: none;"
>
<i class="fas fa-lock me-2"></i>
Зашифровать
</button>
</div>
<textarea
name="content"
id="vars-content"
rows="20"
class="form-control font-monospace"
required
>{{ files_content.get('vars', '') }}</textarea>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof CodeEditor !== 'undefined') {
const editor = CodeEditor.init('vars-content', 'yaml', {
theme: 'monokai',
lineNumbers: true
});
if (editor) {
editor.on('change', function() {
const validation = CodeEditor.validateYAML(editor.getValue());
if (!validation.valid) {
CodeEditor.showErrors(editor, validation.errors);
}
});
}
}
});
</script>
<div class="form-text">
<i class="fas fa-info-circle me-1"></i>
Для работы с Vault файлами используйте кнопки выше
</div>
</div>
<div id="vars-result"></div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
Сохранить
</button>
</form>
</div>
<!-- Meta -->
<div class="tab-pane fade" id="meta" role="tabpanel">
<form
hx-post="/api/v1/roles/{{ role.name }}/update"
hx-target="#meta-result"
hx-swap="innerHTML"
class="mb-3"
>
<input type="hidden" name="file_type" value="meta">
<div class="mb-3">
<label class="form-label">meta/main.yml</label>
<textarea
name="content"
id="meta-content"
rows="20"
class="form-control font-monospace"
required
>{{ files_content.get('meta', '') }}</textarea>
</div>
<div id="meta-result"></div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
Сохранить
</button>
</form>
</div>
<!-- README -->
<div class="tab-pane fade" id="readme" role="tabpanel">
<form
hx-post="/api/v1/roles/{{ role.name }}/update"
hx-target="#readme-result"
hx-swap="innerHTML"
class="mb-3"
>
<input type="hidden" name="file_type" value="readme">
<div class="mb-3">
<label class="form-label">README.md</label>
<textarea
name="content"
rows="20"
class="form-control font-monospace"
required
>{{ readme_content }}</textarea>
</div>
<div id="readme-result"></div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
Сохранить
</button>
</form>
</div>
</div>
</div>
</div>
{% block scripts %}
<script>
async function checkVaultEncryption(fileType) {
const textarea = document.getElementById(fileType + '-content');
if (!textarea) return;
const content = textarea.value;
if (content.includes('$ANSIBLE_VAULT') || content.includes('!vault |')) {
document.getElementById('decrypt-' + fileType + '-btn').style.display = 'inline-block';
document.getElementById('encrypt-' + fileType + '-btn').style.display = 'none';
} else {
document.getElementById('encrypt-' + fileType + '-btn').style.display = 'inline-block';
document.getElementById('decrypt-' + fileType + '-btn').style.display = 'none';
}
}
async function decryptVault(fileType) {
const textarea = document.getElementById(fileType + '-content');
if (!textarea) return;
const content = textarea.value;
if (!content.trim()) {
alert('Нет содержимого для расшифровки');
return;
}
try {
const response = await fetch(`/api/v1/vault/decrypt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ content: content })
});
const result = await response.json();
if (result.success) {
textarea.value = result.decrypted;
document.getElementById('decrypt-' + fileType + '-btn').style.display = 'none';
document.getElementById('encrypt-' + fileType + '-btn').style.display = 'inline-block';
alert('Файл успешно расшифрован');
} else {
alert('Ошибка расшифровки: ' + (result.error || 'Неизвестная ошибка'));
}
} catch (error) {
alert('Ошибка: ' + error.message);
}
}
async function encryptVault(fileType) {
const textarea = document.getElementById(fileType + '-content');
if (!textarea) return;
const content = textarea.value;
if (!content.trim()) {
alert('Нет содержимого для шифрования');
return;
}
const confirmed = await showConfirmModal('Зашифровать содержимое через Ansible Vault?');
if (!confirmed) {
return;
}
try {
const response = await fetch(`/api/v1/vault/encrypt`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ content: content })
});
const result = await response.json();
if (result.success) {
textarea.value = result.encrypted;
document.getElementById('encrypt-' + fileType + '-btn').style.display = 'none';
document.getElementById('decrypt-' + fileType + '-btn').style.display = 'inline-block';
alert('Файл успешно зашифрован');
} else {
alert('Ошибка шифрования: ' + (result.error || 'Неизвестная ошибка'));
}
} catch (error) {
alert('Ошибка: ' + error.message);
}
}
// Инициализация редакторов для всех textarea
document.addEventListener('DOMContentLoaded', function() {
// Инициализация CodeMirror для всех YAML редакторов
if (typeof CodeEditor !== 'undefined') {
const editors = ['tasks', 'handlers', 'defaults', 'vars', 'meta'];
editors.forEach(function(fileType) {
const textareaId = fileType + '-content';
const textarea = document.getElementById(textareaId);
if (textarea) {
const editor = CodeEditor.init(textareaId, 'yaml', {
theme: 'monokai',
lineNumbers: true,
foldGutter: true
});
if (editor) {
editor.on('change', function() {
const content = editor.getValue();
const validation = CodeEditor.validateYAML(content);
if (!validation.valid && content.trim()) {
CodeEditor.showErrors(editor, validation.errors);
} else {
// Очищаем ошибки если валидация прошла
if (editor._validationMarkers) {
editor._validationMarkers.forEach(m => m.clear());
editor._validationMarkers = [];
}
}
});
}
}
});
}
checkVaultEncryption('vars');
// Проверка при переключении на вкладку vars
const varsTab = document.getElementById('vars-tab');
if (varsTab) {
varsTab.addEventListener('shown.bs.tab', function() {
checkVaultEncryption('vars');
});
}
});
</script>
{% endblock %}
{% endblock %}