feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
456
app/templates/pages/roles/edit.html
Normal file
456
app/templates/pages/roles/edit.html
Normal file
@@ -0,0 +1,456 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user