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

519 lines
21 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 %}Редактировать {{ preset.name }} - DevOpsLab{% endblock %}
{% block page_title %}Редактирование preset'а: {{ preset.name }}{% endblock %}
{% block header_actions %}
<a href="/presets/{{ preset.name }}?category={{ preset.category }}" class="btn btn-secondary btn-sm">
<i class="fas fa-arrow-left me-2"></i>
Назад
</a>
{% endblock %}
{% block content %}
<div x-data="presetEditor()">
<form
hx-post="/api/v1/presets/{{ preset.name }}/update?category={{ preset.category }}"
hx-target="#result"
hx-swap="innerHTML"
@submit.prevent="submitForm"
class="card"
>
<!-- Базовая информация -->
<div class="card-header">
<h5 class="mb-0">Базовая информация</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Имя preset'а</label>
<input
type="text"
value="{{ preset.name }}"
class="form-control"
readonly
disabled
>
<div class="form-text">Имя preset'а нельзя изменить</div>
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea
name="description"
x-model="formData.description"
rows="2"
class="form-control"
placeholder="Описание preset'а..."
>{{ preset.data.description if preset.data and preset.data.description else '' }}</textarea>
</div>
<div class="mb-3">
<label class="form-label">Категория</label>
<input
type="text"
value="{{ preset.category }}"
class="form-control"
readonly
disabled
>
<div class="form-text">Категорию нельзя изменить</div>
</div>
<div class="mb-3">
<label class="form-label">Docker Network</label>
<input
type="text"
name="docker_network"
x-model="formData.docker_network"
class="form-control"
placeholder="labnet"
>
</div>
</div>
<!-- Хосты -->
<div class="card-header">
<h5 class="mb-0">Хосты</h5>
</div>
<div class="card-body">
<div class="space-y-2 mb-3" x-ref="hostsContainer">
<template x-for="(host, index) in formData.hosts" :key="index">
<div class="card mb-2">
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-12 col-md-2">
<label class="form-label small">Имя хоста</label>
<input
type="text"
x-model="host.name"
placeholder="u1"
class="form-control"
required
>
</div>
<div class="col-12 col-md-3">
<label class="form-label small">Семейство образа</label>
<select
x-model="host.family"
class="form-select"
>
<option value="ubuntu20">Ubuntu 20</option>
<option value="ubuntu22">Ubuntu 22</option>
<option value="ubuntu24">Ubuntu 24</option>
<option value="debian9">Debian 9</option>
<option value="debian10">Debian 10</option>
<option value="debian11">Debian 11</option>
<option value="debian12">Debian 12</option>
<option value="centos7">CentOS 7</option>
<option value="centos8">CentOS 8</option>
<option value="centos9">CentOS 9</option>
<option value="alma">AlmaLinux</option>
<option value="rocky">Rocky Linux</option>
<option value="rhel">RHEL</option>
<option value="redos">RedOS</option>
<option value="astra">Astra Linux</option>
<option value="alt9">Alt Linux 9</option>
<option value="alt10">Alt Linux 10</option>
</select>
</div>
<div class="col-12 col-md-3">
<label class="form-label small">Группы (через запятую)</label>
<input
type="text"
x-model="host.groups_str"
placeholder="test, web"
class="form-control"
@input="updateHostGroups(index)"
>
</div>
<div class="col-12 col-md-2">
<label class="form-label small">Тип (опционально)</label>
<select
x-model="host.type"
class="form-select"
>
<option value="">Обычный</option>
<option value="dind">DinD</option>
<option value="dood">DOoD</option>
</select>
</div>
<div class="col-12 col-md-2">
<button
type="button"
@click="removeHost(index)"
class="btn btn-danger btn-sm w-100"
title="Удалить"
>
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</template>
</div>
<button
type="button"
@click="addHost"
class="btn btn-outline-secondary"
>
<i class="fas fa-plus me-2"></i>
Добавить хост
</button>
</div>
<!-- Образы -->
<div class="card-header">
<h5 class="mb-0">Docker образы</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="space-y-2" x-ref="imagesContainer">
<template x-for="(image, key) in formData.images" :key="key">
<div class="row g-2 mb-2 align-items-center">
<div class="col-12 col-md-4">
<input
type="text"
:value="key"
class="form-control"
readonly
disabled
>
</div>
<div class="col-12 col-md-6">
<input
type="text"
x-model="formData.images[key]"
class="form-control"
placeholder="inecs/ansible-lab:ubuntu22-latest"
>
</div>
<div class="col-12 col-md-2">
<button
type="button"
@click="removeImage(key)"
class="btn btn-danger btn-sm w-100"
title="Удалить"
>
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</template>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-12 col-md-4">
<input
type="text"
x-model="newImageKey"
class="form-control"
placeholder="Новый ключ (ubuntu25)"
>
</div>
<div class="col-12 col-md-6">
<input
type="text"
x-model="newImageValue"
class="form-control"
placeholder="inecs/ansible-lab:ubuntu25-latest"
>
</div>
<div class="col-12 col-md-2">
<button
type="button"
@click="addImage"
class="btn btn-outline-primary w-100"
>
<i class="fas fa-plus"></i>
</button>
</div>
</div>
</div>
<!-- Systemd Defaults -->
<div class="card-header">
<h5 class="mb-0">Systemd Defaults</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-6">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
x-model="formData.systemd_defaults.privileged"
id="privileged"
>
<label class="form-check-label" for="privileged">
Privileged
</label>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label">Command</label>
<input
type="text"
x-model="formData.systemd_defaults.command"
class="form-control"
placeholder="/sbin/init"
>
</div>
<div class="col-12">
<label class="form-label">Volumes (по одному на строку)</label>
<textarea
x-model="formData.systemd_defaults.volumes_str"
@input="updateVolumes"
rows="3"
class="form-control"
placeholder="/sys/fs/cgroup:/sys/fs/cgroup:rw"
></textarea>
</div>
<div class="col-12">
<label class="form-label">Tmpfs (через запятую)</label>
<input
type="text"
x-model="formData.systemd_defaults.tmpfs_str"
@input="updateTmpfs"
class="form-control"
placeholder="/run, /run/lock"
>
</div>
<div class="col-12">
<label class="form-label">Capabilities (через запятую)</label>
<input
type="text"
x-model="formData.systemd_defaults.capabilities_str"
@input="updateCapabilities"
class="form-control"
placeholder="SYS_ADMIN"
>
</div>
</div>
</div>
{% if preset.category == 'k8s' %}
<!-- Kind Clusters (только для k8s) -->
<div class="card-header">
<h5 class="mb-0">Kind Clusters</h5>
</div>
<div class="card-body">
<div class="mb-3">
<div class="space-y-2" x-ref="clustersContainer">
<template x-for="(cluster, index) in formData.kind_clusters" :key="index">
<div class="row g-2 mb-2 align-items-center">
<div class="col-12 col-md-10">
<input
type="text"
x-model="formData.kind_clusters[index]"
class="form-control"
placeholder="cluster-name"
>
</div>
<div class="col-12 col-md-2">
<button
type="button"
@click="removeCluster(index)"
class="btn btn-danger btn-sm w-100"
title="Удалить"
>
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</template>
</div>
</div>
<button
type="button"
@click="addCluster"
class="btn btn-outline-secondary"
>
<i class="fas fa-plus me-2"></i>
Добавить кластер
</button>
</div>
{% endif %}
<!-- Скрытые поля для отправки -->
<input
type="hidden"
name="hosts"
:value="JSON.stringify(formData.hosts.map(h => ({
name: h.name,
family: h.family,
groups: h.groups,
type: h.type || undefined,
supported_platforms: h.supported_platforms || undefined
})))"
>
<input
type="hidden"
name="images"
:value="JSON.stringify(formData.images)"
>
<input
type="hidden"
name="systemd_defaults"
:value="JSON.stringify({
privileged: formData.systemd_defaults.privileged,
command: formData.systemd_defaults.command,
volumes: formData.systemd_defaults.volumes,
tmpfs: formData.systemd_defaults.tmpfs,
capabilities: formData.systemd_defaults.capabilities
})"
>
{% if preset.category == 'k8s' %}
<input
type="hidden"
name="kind_clusters"
:value="JSON.stringify(formData.kind_clusters)"
>
{% endif %}
<!-- Результат -->
<div id="result" class="card-body border-top"></div>
<!-- Кнопки -->
<div class="card-footer">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>
Сохранить изменения
</button>
<a href="/presets/{{ preset.name }}?category={{ preset.category }}" class="btn btn-secondary">
<i class="fas fa-times me-2"></i>
Отмена
</a>
</div>
</div>
</form>
</div>
<script>
function presetEditor() {
// Парсинг данных preset'а из шаблона
const presetData = {% if preset.data %}{{ preset.data | tojson }}{% else %}{}{% endif %};
// Инициализация хостов
const hosts = presetData.hosts || [];
const formattedHosts = hosts.map(host => ({
name: host.name || '',
family: host.family || 'ubuntu22',
groups: host.groups || [],
groups_str: Array.isArray(host.groups) ? host.groups.join(', ') : (host.groups || ''),
type: host.type || '',
supported_platforms: host.supported_platforms || []
}));
// Инициализация образов
const images = presetData.images || {};
// Инициализация systemd defaults
const systemdDefaults = presetData.systemd_defaults || {};
const volumes = systemdDefaults.volumes || [];
const tmpfs = systemdDefaults.tmpfs || [];
const capabilities = systemdDefaults.capabilities || [];
// Извлечение описания из комментария в content
let description = '';
{% if preset.content %}
const contentLines = {{ preset.content | tojson }}.split('\n');
for (const line of contentLines) {
if (line.trim().startsWith('#description:')) {
description = line.split('#description:')[1].trim();
break;
}
}
{% endif %}
return {
formData: {
description: description,
docker_network: presetData.docker_network || 'labnet',
hosts: formattedHosts.length > 0 ? formattedHosts : [{
name: 'u1',
family: 'ubuntu22',
groups: ['test'],
groups_str: 'test',
type: ''
}],
images: images,
systemd_defaults: {
privileged: systemdDefaults.privileged !== undefined ? systemdDefaults.privileged : true,
command: systemdDefaults.command || '/sbin/init',
volumes: volumes,
volumes_str: volumes.join('\n'),
tmpfs: tmpfs,
tmpfs_str: tmpfs.join(', '),
capabilities: capabilities,
capabilities_str: capabilities.join(', ')
},
kind_clusters: presetData.kind_clusters || []
},
newImageKey: '',
newImageValue: '',
addHost() {
this.formData.hosts.push({
name: `u${this.formData.hosts.length + 1}`,
family: 'ubuntu22',
groups: ['test'],
groups_str: 'test',
type: ''
});
},
removeHost(index) {
this.formData.hosts.splice(index, 1);
},
updateHostGroups(index) {
const host = this.formData.hosts[index];
host.groups = host.groups_str.split(',').map(g => g.trim()).filter(g => g);
},
addImage() {
if (this.newImageKey && this.newImageValue) {
this.formData.images[this.newImageKey] = this.newImageValue;
this.newImageKey = '';
this.newImageValue = '';
}
},
removeImage(key) {
delete this.formData.images[key];
},
updateVolumes() {
this.formData.systemd_defaults.volumes =
this.formData.systemd_defaults.volumes_str
.split('\n')
.map(v => v.trim())
.filter(v => v);
},
updateTmpfs() {
this.formData.systemd_defaults.tmpfs =
this.formData.systemd_defaults.tmpfs_str
.split(',')
.map(t => t.trim())
.filter(t => t);
},
updateCapabilities() {
this.formData.systemd_defaults.capabilities =
this.formData.systemd_defaults.capabilities_str
.split(',')
.map(c => c.trim())
.filter(c => c);
},
addCluster() {
this.formData.kind_clusters.push('');
},
removeCluster(index) {
this.formData.kind_clusters.splice(index, 1);
},
submitForm(event) {
// HTMX обработает отправку
}
}
}
</script>
{% endblock %}