Files
octopus/myproject/system_settings/templates/system_settings/integrations.html

398 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 "system_settings/base_settings.html" %}
{% block title %}Интеграции{% endblock %}
{% block settings_content %}
<div class="row">
<!-- Левая колонка: список интеграций с тумблерами -->
<div class="col-md-5">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0">Доступные интеграции</h5>
</div>
<div class="card-body p-0">
{% for key, integration in integrations.items %}
<div class="integration-item d-flex align-items-center justify-content-between p-3 border-bottom {% if integration.is_active %}bg-success bg-opacity-10{% endif %}"
data-integration="{{ key }}"
style="cursor: pointer;">
<div class="d-flex align-items-center">
<div>
<span class="fw-medium">{{ integration.label }}</span>
<small class="text-muted d-block">{{ integration.category }}</small>
</div>
{% if integration.is_configured %}
<span class="badge bg-success ms-2" title="Настроена">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</svg>
</span>
{% endif %}
</div>
<div class="form-check form-switch m-0">
<input class="form-check-input integration-toggle"
type="checkbox"
data-integration="{{ key }}"
id="toggle-{{ key }}"
{% if integration.is_active %}checked{% endif %}
style="cursor: pointer;">
</div>
</div>
{% empty %}
<div class="p-4 text-center text-muted">
Нет доступных интеграций
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Правая колонка: форма настроек выбранной интеграции -->
<div class="col-md-7">
<div class="card shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="mb-0" id="settings-title">Настройки интеграции</h5>
<span class="badge bg-secondary" id="settings-status" style="display: none;"></span>
</div>
<div class="card-body">
<!-- Placeholder когда ничего не выбрано -->
<div id="settings-placeholder" class="text-center text-muted py-5">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" class="bi bi-plug mb-3" viewBox="0 0 16 16">
<path d="M6 0a.5.5 0 0 1 .5.5V3h3V.5a.5.5 0 0 1 1 0V3h1a.5.5 0 0 1 .5.5v3A3.5 3.5 0 0 1 8.5 10c-.002.434-.01.845-.04 1.22-.041.514-.126 1.003-.317 1.424a2.083 2.083 0 0 1-.97 1.028C6.725 13.9 6.169 14 5.5 14c-.998 0-1.61.33-1.974.718A1.922 1.922 0 0 0 3 16H2c0-.616.232-1.367.797-1.968C3.374 13.42 4.261 13 5.5 13c.581 0 .962-.088 1.218-.219.241-.123.4-.3.514-.55.121-.266.193-.621.23-1.09.027-.34.035-.718.037-1.141A3.5 3.5 0 0 1 4 6.5v-3a.5.5 0 0 1 .5-.5h1V.5A.5.5 0 0 1 6 0z"/>
</svg>
<p class="mb-0">Выберите интеграцию слева для настройки</p>
<small class="text-muted">Кликните на название или тумблер</small>
</div>
<!-- Форма настроек (скрыта по умолчанию) -->
<div id="settings-form-container" style="display: none;">
<form id="settings-form">
<div id="settings-fields">
<!-- Поля генерируются динамически -->
</div>
<hr>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">
Сохранить настройки
</button>
<button type="button" class="btn btn-outline-secondary" id="test-connection-btn" style="display: none;">
Проверить подключение
</button>
</div>
</form>
</div>
<!-- Индикатор загрузки -->
<div id="settings-loading" class="text-center py-5" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Toast для уведомлений -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="notification-toast" class="toast" role="alert">
<div class="toast-header">
<strong class="me-auto" id="toast-title">Уведомление</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body" id="toast-message"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const csrfToken = '{{ csrf_token }}';
let currentIntegration = null;
// Toast helper
function showToast(title, message, isError = false) {
const toast = document.getElementById('notification-toast');
document.getElementById('toast-title').textContent = title;
document.getElementById('toast-message').textContent = message;
toast.classList.toggle('bg-danger', isError);
toast.classList.toggle('text-white', isError);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
}
// Загрузка формы настроек
async function loadSettingsForm(integrationId) {
currentIntegration = integrationId;
// Показать загрузку
document.getElementById('settings-placeholder').style.display = 'none';
document.getElementById('settings-form-container').style.display = 'none';
document.getElementById('settings-loading').style.display = 'block';
try {
const response = await fetch(`/settings/integrations/form/${integrationId}/`);
const data = await response.json();
if (data.error) {
showToast('Ошибка', data.error, true);
return;
}
// Обновить заголовок
const integrationData = {{ integrations_json|safe }};
document.getElementById('settings-title').textContent =
`Настройки: ${integrationData[integrationId]?.label || integrationId}`;
// Обновить статус
const statusBadge = document.getElementById('settings-status');
if (data.is_active) {
statusBadge.textContent = 'Включена';
statusBadge.className = 'badge bg-success';
} else {
statusBadge.textContent = 'Выключена';
statusBadge.className = 'badge bg-secondary';
}
statusBadge.style.display = 'inline';
// Построить форму
buildForm(data.fields, data.data || {});
// Показать/скрыть кнопку тестирования
const testBtn = document.getElementById('test-connection-btn');
testBtn.style.display = data.is_configured ? 'inline-block' : 'none';
// Показать форму
document.getElementById('settings-loading').style.display = 'none';
document.getElementById('settings-form-container').style.display = 'block';
} catch (error) {
console.error('Error loading form:', error);
showToast('Ошибка', 'Не удалось загрузить настройки', true);
document.getElementById('settings-loading').style.display = 'none';
document.getElementById('settings-placeholder').style.display = 'block';
}
}
// Построение формы из метаданных полей
function buildForm(fields, data) {
const container = document.getElementById('settings-fields');
container.innerHTML = '';
fields.forEach(field => {
const div = document.createElement('div');
div.className = 'mb-3';
if (field.type === 'checkbox') {
div.className = 'form-check mb-3';
div.innerHTML = `
<input type="checkbox" class="form-check-input" id="field-${field.name}"
name="${field.name}" ${data[field.name] ? 'checked' : ''}>
<label class="form-check-label" for="field-${field.name}">${field.label}</label>
${field.help_text ? `<div class="form-text">${field.help_text}</div>` : ''}
`;
} else if (field.type === 'select') {
div.innerHTML = `
<label class="form-label" for="field-${field.name}">
${field.label}
${field.required ? '<span class="text-danger">*</span>' : ''}
</label>
<select class="form-select" id="field-${field.name}"
name="${field.name}"
${field.required ? 'required' : ''}>
${field.choices.map(choice => `
<option value="${choice[0]}" ${data[field.name] === choice[0] ? 'selected' : ''}>
${choice[1]}
</option>
`).join('')}
</select>
${field.help_text ? `<div class="form-text">${field.help_text}</div>` : ''}
`;
} else {
const inputType = field.type === 'password' ? 'password' : (field.type === 'url' ? 'url' : 'text');
const value = data[field.name] || '';
const placeholder = field.type === 'password' && value === '........' ? 'Введите новое значение для изменения' : '';
div.innerHTML = `
<label class="form-label" for="field-${field.name}">
${field.label}
${field.required ? '<span class="text-danger">*</span>' : ''}
</label>
<input type="${inputType}" class="form-control" id="field-${field.name}"
name="${field.name}" value="${value !== '........' ? value : ''}"
placeholder="${placeholder}"
${field.required ? 'required' : ''}>
${field.help_text ? `<div class="form-text">${field.help_text}</div>` : ''}
`;
}
container.appendChild(div);
});
}
// Обработчик клика на интеграцию
document.querySelectorAll('.integration-item').forEach(item => {
item.addEventListener('click', function(e) {
// Не обрабатывать клик на самом тумблере
if (e.target.classList.contains('integration-toggle')) return;
const integrationId = this.dataset.integration;
loadSettingsForm(integrationId);
// Подсветить выбранный
document.querySelectorAll('.integration-item').forEach(i => i.classList.remove('border-primary'));
this.classList.add('border-primary');
});
});
// Обработчик тумблера
document.querySelectorAll('.integration-toggle').forEach(toggle => {
toggle.addEventListener('change', async function(e) {
e.stopPropagation();
const integrationId = this.dataset.integration;
const checkbox = this;
try {
const response = await fetch(`/settings/integrations/toggle/${integrationId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (!response.ok) {
// Откатить состояние
checkbox.checked = !checkbox.checked;
showToast('Ошибка', data.error || 'Не удалось изменить состояние', true);
// Открыть настройки если не настроено
if (!data.is_configured) {
loadSettingsForm(integrationId);
}
return;
}
// Обновить UI
const item = checkbox.closest('.integration-item');
if (data.is_active) {
item.classList.add('bg-success', 'bg-opacity-10');
showToast('Успех', 'Интеграция включена');
} else {
item.classList.remove('bg-success', 'bg-opacity-10');
showToast('Успех', 'Интеграция выключена');
}
// Обновить статус в форме если открыта
if (currentIntegration === integrationId) {
const statusBadge = document.getElementById('settings-status');
if (data.is_active) {
statusBadge.textContent = 'Включена';
statusBadge.className = 'badge bg-success';
} else {
statusBadge.textContent = 'Выключена';
statusBadge.className = 'badge bg-secondary';
}
}
} catch (error) {
checkbox.checked = !checkbox.checked;
showToast('Ошибка', 'Ошибка сети', true);
}
});
});
// Сохранение настроек
document.getElementById('settings-form').addEventListener('submit', async function(e) {
e.preventDefault();
if (!currentIntegration) return;
const formData = new FormData(this);
const data = {};
// Собрать данные формы
for (const [key, value] of formData.entries()) {
// Пропустить пустые password поля (не менять если не введено)
const input = document.getElementById(`field-${key}`);
if (input && input.type === 'password' && !value) continue;
data[key] = value;
}
// Добавить checkbox'ы (они не попадают в FormData если не checked)
this.querySelectorAll('input[type="checkbox"]').forEach(cb => {
data[cb.name] = cb.checked;
});
try {
const response = await fetch(`/settings/integrations/settings/${currentIntegration}/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (!response.ok) {
showToast('Ошибка', result.error || 'Не удалось сохранить', true);
return;
}
showToast('Успех', 'Настройки сохранены');
// Обновить бейдж "настроено" в списке
const item = document.querySelector(`.integration-item[data-integration="${currentIntegration}"]`);
if (result.is_configured) {
if (!item.querySelector('.badge.bg-success')) {
const badge = document.createElement('span');
badge.className = 'badge bg-success ms-2';
badge.title = 'Настроена';
badge.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16"><path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/></svg>';
item.querySelector('.fw-medium').after(badge);
}
}
} catch (error) {
showToast('Ошибка', 'Ошибка сети', true);
}
});
// Тестирование соединения
document.getElementById('test-connection-btn').addEventListener('click', async function() {
if (!currentIntegration) return;
const btn = this;
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Проверка...';
try {
const response = await fetch(`/settings/integrations/test/${currentIntegration}/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
showToast('Успех', data.message);
} else {
showToast('Ошибка', data.message || data.error, true);
}
} catch (error) {
showToast('Ошибка', 'Ошибка сети', true);
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
});
});
</script>
{% endblock %}