463 lines
19 KiB
HTML
463 lines
19 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}System Configuration - {{ block.super }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-1">
|
|
<i class="fas fa-cogs me-2"></i>System Configuration
|
|
</h1>
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb mb-0">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item active">System Configuration</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
|
<i class="fas fa-cog me-2"></i>Actions
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="#" onclick="exportConfigurations()">
|
|
<i class="fas fa-download me-2"></i>Export Configuration
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="importConfigurations()">
|
|
<i class="fas fa-upload me-2"></i>Import Configuration
|
|
</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item" href="#" onclick="resetToDefaults()">
|
|
<i class="fas fa-undo me-2"></i>Reset to Defaults
|
|
</a></li>
|
|
</ul>
|
|
<button type="button" class="btn btn-primary" onclick="addConfiguration()">
|
|
<i class="fas fa-plus me-2"></i>Add Configuration
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search and Filter -->
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" placeholder="Search configurations..."
|
|
id="configSearch" onkeyup="filterConfigurations()">
|
|
<span class="input-group-text">
|
|
<i class="fas fa-search"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select class="form-select" id="categoryFilter" onchange="filterConfigurations()">
|
|
<option value="">All Categories</option>
|
|
{% for category in categories %}
|
|
<option value="{{ category }}">{{ category|title }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select class="form-select" id="scopeFilter" onchange="filterConfigurations()">
|
|
<option value="">All Scopes</option>
|
|
<option value="global">Global</option>
|
|
<option value="tenant">Tenant-specific</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button type="button" class="btn btn-outline-secondary w-100" onclick="clearFilters()">
|
|
<i class="fas fa-times me-1"></i>Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Configuration Categories -->
|
|
{% if grouped_configurations %}
|
|
{% for category, configs in grouped_configurations.items %}
|
|
<div class="config-category mb-4" data-category="{{ category|lower }}">
|
|
<div class="card">
|
|
<div class="card-header bg-primary text-white">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-folder me-2"></i>{{ category|title }}
|
|
</h5>
|
|
<span class="badge bg-light text-dark">{{ configs|length }} item{{ configs|length|pluralize }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body p-0">
|
|
{% for config in configs %}
|
|
<div class="config-item border-bottom p-3 {% if config.is_sensitive %}bg-warning-subtle{% endif %} {% if config.is_readonly %}bg-light{% endif %}"
|
|
data-key="{{ config.key|lower }}" data-scope="{% if config.tenant %}tenant{% else %}global{% endif %}">
|
|
<div class="row align-items-start">
|
|
<div class="col-lg-3">
|
|
<div class="mb-2">
|
|
<code class="config-key">{{ config.key }}</code>
|
|
</div>
|
|
|
|
<div class="d-flex flex-wrap gap-1">
|
|
<span class="badge bg-primary">{{ config.data_type }}</span>
|
|
|
|
{% if config.is_sensitive %}
|
|
<span class="badge bg-warning">
|
|
<i class="fas fa-lock me-1"></i>Sensitive
|
|
</span>
|
|
{% endif %}
|
|
|
|
{% if config.is_readonly %}
|
|
<span class="badge bg-secondary">
|
|
<i class="fas fa-eye me-1"></i>Read-only
|
|
</span>
|
|
{% endif %}
|
|
|
|
{% if config.is_encrypted %}
|
|
<span class="badge bg-dark">
|
|
<i class="fas fa-shield-alt me-1"></i>Encrypted
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-6">
|
|
{% if config.description %}
|
|
<div class="mb-2">
|
|
<small class="text-muted">{{ config.description }}</small>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="config-value p-2 bg-light rounded border">
|
|
{% if config.is_sensitive and not config.is_encrypted %}
|
|
<span class="text-muted">
|
|
<i class="fas fa-eye-slash me-1"></i>[Hidden for security]
|
|
</span>
|
|
{% elif config.is_encrypted %}
|
|
<span class="text-muted">
|
|
<i class="fas fa-lock me-1"></i>[Encrypted value]
|
|
</span>
|
|
{% else %}
|
|
<code>{{ config.value|default:"<em>No value set</em>" }}</code>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if config.default_value %}
|
|
<div class="mt-2">
|
|
<small class="text-muted">
|
|
<strong>Default:</strong> <code>{{ config.default_value }}</code>
|
|
</small>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="col-lg-3">
|
|
<div class="d-flex justify-content-end mb-2">
|
|
{% if not config.is_readonly %}
|
|
<button class="btn btn-sm btn-outline-primary me-1"
|
|
onclick="editConfiguration('{{ config.id }}')"
|
|
title="Edit Configuration">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
{% endif %}
|
|
|
|
<button class="btn btn-sm btn-outline-info me-1"
|
|
onclick="viewConfigDetails('{{ config.id }}')"
|
|
title="View Details">
|
|
<i class="fas fa-info-circle"></i>
|
|
</button>
|
|
|
|
{% if config.validation_rules %}
|
|
<button class="btn btn-sm btn-outline-secondary"
|
|
onclick="showValidationRules('{{ config.id }}')"
|
|
title="Validation Rules">
|
|
<i class="fas fa-check-circle"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="text-end">
|
|
<small class="text-muted d-block">
|
|
{% if config.tenant %}
|
|
<i class="fas fa-building me-1"></i>{{ config.tenant.name }}
|
|
{% else %}
|
|
<i class="fas fa-globe me-1"></i>Global
|
|
{% endif %}
|
|
</small>
|
|
|
|
{% if config.required_permission %}
|
|
<small class="text-muted d-block">
|
|
<i class="fas fa-key me-1"></i>{{ config.required_permission }}
|
|
</small>
|
|
{% endif %}
|
|
|
|
<small class="text-muted d-block">
|
|
<i class="fas fa-clock me-1"></i>{{ config.updated_at|date:"M d, Y H:i" }}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if config.validation_rules %}
|
|
<div class="mt-3 pt-3 border-top">
|
|
<div class="collapse" id="validation-{{ config.id }}">
|
|
<h6 class="text-muted">
|
|
<i class="fas fa-rules me-1"></i>Validation Rules
|
|
</h6>
|
|
<pre class="bg-light p-2 rounded"><code>{{ config.validation_rules|pprint }}</code></pre>
|
|
</div>
|
|
<button class="btn btn-sm btn-link text-muted p-0"
|
|
data-bs-toggle="collapse"
|
|
data-bs-target="#validation-{{ config.id }}">
|
|
<i class="fas fa-chevron-down me-1"></i>Show Validation Rules
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="card">
|
|
<div class="card-body text-center py-5">
|
|
<i class="fas fa-cogs fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No configurations found</h5>
|
|
<p class="text-muted">No system configurations are currently available.</p>
|
|
<button type="button" class="btn btn-primary" onclick="addConfiguration()">
|
|
<i class="fas fa-plus me-2"></i>Add First Configuration
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Configuration Modal -->
|
|
<div class="modal fade" id="configModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Configuration Details</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="configModalBody">
|
|
<!-- Content loaded dynamically -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Configuration management functionality
|
|
function filterConfigurations() {
|
|
const searchTerm = document.getElementById('configSearch').value.toLowerCase();
|
|
const categoryFilter = document.getElementById('categoryFilter').value.toLowerCase();
|
|
const scopeFilter = document.getElementById('scopeFilter').value;
|
|
|
|
const categories = document.querySelectorAll('.config-category');
|
|
|
|
categories.forEach(category => {
|
|
const categoryName = category.dataset.category;
|
|
let categoryVisible = false;
|
|
|
|
// Check if category matches filter
|
|
if (categoryFilter && categoryName !== categoryFilter) {
|
|
category.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const items = category.querySelectorAll('.config-item');
|
|
items.forEach(item => {
|
|
const key = item.dataset.key;
|
|
const scope = item.dataset.scope;
|
|
const text = item.textContent.toLowerCase();
|
|
|
|
const matchesSearch = !searchTerm || text.includes(searchTerm);
|
|
const matchesScope = !scopeFilter || scope === scopeFilter;
|
|
|
|
if (matchesSearch && matchesScope) {
|
|
item.style.display = 'block';
|
|
categoryVisible = true;
|
|
} else {
|
|
item.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
category.style.display = categoryVisible ? 'block' : 'none';
|
|
});
|
|
}
|
|
|
|
function clearFilters() {
|
|
document.getElementById('configSearch').value = '';
|
|
document.getElementById('categoryFilter').value = '';
|
|
document.getElementById('scopeFilter').value = '';
|
|
filterConfigurations();
|
|
}
|
|
|
|
function editConfiguration(configId) {
|
|
// Load configuration edit form
|
|
fetch(`/core/configurations/${configId}/edit/`)
|
|
.then(response => response.text())
|
|
.then(html => {
|
|
document.getElementById('configModalBody').innerHTML = html;
|
|
new bootstrap.Modal(document.getElementById('configModal')).show();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading configuration:', error);
|
|
alert('Error loading configuration details');
|
|
});
|
|
}
|
|
|
|
function viewConfigDetails(configId) {
|
|
// Load configuration details
|
|
fetch(`/core/configurations/${configId}/`)
|
|
.then(response => response.text())
|
|
.then(html => {
|
|
document.getElementById('configModalBody').innerHTML = html;
|
|
new bootstrap.Modal(document.getElementById('configModal')).show();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading configuration:', error);
|
|
alert('Error loading configuration details');
|
|
});
|
|
}
|
|
|
|
function showValidationRules(configId) {
|
|
const validationElement = document.getElementById(`validation-${configId}`);
|
|
if (validationElement) {
|
|
new bootstrap.Collapse(validationElement).toggle();
|
|
}
|
|
}
|
|
|
|
function addConfiguration() {
|
|
// Load new configuration form
|
|
fetch('/core/configurations/add/')
|
|
.then(response => response.text())
|
|
.then(html => {
|
|
document.getElementById('configModalBody').innerHTML = html;
|
|
new bootstrap.Modal(document.getElementById('configModal')).show();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading form:', error);
|
|
alert('Error loading configuration form');
|
|
});
|
|
}
|
|
|
|
function exportConfigurations() {
|
|
const categoryFilter = document.getElementById('categoryFilter').value;
|
|
const scopeFilter = document.getElementById('scopeFilter').value;
|
|
|
|
const params = new URLSearchParams();
|
|
if (categoryFilter) params.append('category', categoryFilter);
|
|
if (scopeFilter) params.append('scope', scopeFilter);
|
|
|
|
window.open(`/core/configurations/export/?${params}`, '_blank');
|
|
}
|
|
|
|
function importConfigurations() {
|
|
// Implementation for configuration import
|
|
alert('Configuration import functionality would be implemented here');
|
|
}
|
|
|
|
function resetToDefaults() {
|
|
if (confirm('Are you sure you want to reset all configurations to their default values? This action cannot be undone.')) {
|
|
fetch('/core/configurations/reset/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': getCsrfToken()
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Error resetting configurations: ' + data.error);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Error resetting configurations');
|
|
});
|
|
}
|
|
}
|
|
|
|
function getCsrfToken() {
|
|
return document.querySelector('[name=csrfmiddlewaretoken]').value;
|
|
}
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
|
return;
|
|
}
|
|
|
|
switch (e.key) {
|
|
case '/':
|
|
e.preventDefault();
|
|
document.getElementById('configSearch').focus();
|
|
break;
|
|
case 'n':
|
|
e.preventDefault();
|
|
addConfiguration();
|
|
break;
|
|
case 'r':
|
|
e.preventDefault();
|
|
location.reload();
|
|
break;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.config-key {
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
font-size: 0.9rem;
|
|
color: #495057;
|
|
background: #f8f9fa;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
border: 1px solid #dee2e6;
|
|
}
|
|
|
|
.config-value {
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
font-size: 0.9rem;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.config-item {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.config-item:hover {
|
|
background-color: rgba(0, 0, 0, 0.02) !important;
|
|
}
|
|
|
|
.bg-warning-subtle {
|
|
background-color: #fff3cd !important;
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.config-item .row {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.config-item .col-lg-3,
|
|
.config-item .col-lg-6 {
|
|
margin-bottom: 1rem;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|