634 lines
20 KiB
JavaScript
634 lines
20 KiB
JavaScript
/**
|
|
* Config Manager - Handles configuration file upload, management, and display
|
|
* Phase 4: Config Creator
|
|
*/
|
|
|
|
class ConfigManager {
|
|
constructor() {
|
|
this.apiBase = '/api/configs';
|
|
this.currentPreview = null;
|
|
this.currentFilename = null;
|
|
}
|
|
|
|
/**
|
|
* Load all configurations and populate the table
|
|
*/
|
|
async loadConfigs() {
|
|
try {
|
|
const response = await fetch(this.apiBase);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
this.renderConfigsTable(data.configs || []);
|
|
return data.configs;
|
|
} catch (error) {
|
|
console.error('Error loading configs:', error);
|
|
this.showError('Failed to load configurations: ' + error.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a specific configuration file
|
|
*/
|
|
async getConfig(filename) {
|
|
try {
|
|
const response = await fetch(`${this.apiBase}/${encodeURIComponent(filename)}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('Error getting config:', error);
|
|
this.showError('Failed to load configuration: ' + error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Upload CSV file and convert to YAML
|
|
*/
|
|
async uploadCSV(file) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
try {
|
|
const response = await fetch(`${this.apiBase}/upload-csv`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
console.error('Error uploading CSV:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Upload YAML file directly
|
|
*/
|
|
async uploadYAML(file, filename = null) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
if (filename) {
|
|
formData.append('filename', filename);
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${this.apiBase}/upload-yaml`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
console.error('Error uploading YAML:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a configuration file
|
|
*/
|
|
async deleteConfig(filename) {
|
|
try {
|
|
const response = await fetch(`${this.apiBase}/${encodeURIComponent(filename)}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.message || `HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
return data;
|
|
} catch (error) {
|
|
console.error('Error deleting config:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download CSV template
|
|
*/
|
|
downloadTemplate() {
|
|
window.location.href = `${this.apiBase}/template`;
|
|
}
|
|
|
|
/**
|
|
* Download a specific config file
|
|
*/
|
|
downloadConfig(filename) {
|
|
window.location.href = `${this.apiBase}/${encodeURIComponent(filename)}/download`;
|
|
}
|
|
|
|
/**
|
|
* Show YAML preview in the preview pane
|
|
*/
|
|
showPreview(yamlContent, filename = null) {
|
|
this.currentPreview = yamlContent;
|
|
this.currentFilename = filename;
|
|
|
|
const previewElement = document.getElementById('yaml-preview');
|
|
const contentElement = document.getElementById('yaml-content');
|
|
const placeholderElement = document.getElementById('preview-placeholder');
|
|
|
|
if (contentElement) {
|
|
contentElement.textContent = yamlContent;
|
|
}
|
|
|
|
if (previewElement) {
|
|
previewElement.style.display = 'block';
|
|
}
|
|
|
|
if (placeholderElement) {
|
|
placeholderElement.style.display = 'none';
|
|
}
|
|
|
|
// Enable save button
|
|
const saveBtn = document.getElementById('save-config-btn');
|
|
if (saveBtn) {
|
|
saveBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide YAML preview
|
|
*/
|
|
hidePreview() {
|
|
this.currentPreview = null;
|
|
this.currentFilename = null;
|
|
|
|
const previewElement = document.getElementById('yaml-preview');
|
|
const placeholderElement = document.getElementById('preview-placeholder');
|
|
|
|
if (previewElement) {
|
|
previewElement.style.display = 'none';
|
|
}
|
|
|
|
if (placeholderElement) {
|
|
placeholderElement.style.display = 'block';
|
|
}
|
|
|
|
// Disable save button
|
|
const saveBtn = document.getElementById('save-config-btn');
|
|
if (saveBtn) {
|
|
saveBtn.disabled = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render configurations table
|
|
*/
|
|
renderConfigsTable(configs) {
|
|
const tbody = document.querySelector('#configs-table tbody');
|
|
|
|
if (!tbody) {
|
|
console.warn('Configs table body not found');
|
|
return;
|
|
}
|
|
|
|
// Clear existing rows
|
|
tbody.innerHTML = '';
|
|
|
|
if (configs.length === 0) {
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="6" class="text-center text-muted py-4">
|
|
<i class="bi bi-inbox" style="font-size: 2rem;"></i>
|
|
<p class="mt-2">No configuration files found. Create your first config!</p>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Populate table
|
|
configs.forEach(config => {
|
|
const row = document.createElement('tr');
|
|
row.dataset.filename = config.filename;
|
|
|
|
// Format date
|
|
const createdDate = config.created_at ?
|
|
new Date(config.created_at).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
}) : 'Unknown';
|
|
|
|
// Format file size
|
|
const fileSize = config.size_bytes ?
|
|
this.formatFileSize(config.size_bytes) : 'Unknown';
|
|
|
|
// Schedule usage badge
|
|
const scheduleCount = config.used_by_schedules ? config.used_by_schedules.length : 0;
|
|
const scheduleBadge = scheduleCount > 0 ?
|
|
`<span class="schedule-badge" title="${config.used_by_schedules.join(', ')}">${scheduleCount}</span>` :
|
|
'<span class="text-muted">None</span>';
|
|
|
|
row.innerHTML = `
|
|
<td><code>${this.escapeHtml(config.filename)}</code></td>
|
|
<td>${this.escapeHtml(config.title || 'Untitled')}</td>
|
|
<td>${createdDate}</td>
|
|
<td>${fileSize}</td>
|
|
<td>${scheduleBadge}</td>
|
|
<td class="config-actions">
|
|
<button class="btn btn-sm btn-outline-secondary"
|
|
onclick="configManager.viewConfig('${this.escapeHtml(config.filename)}')"
|
|
title="View config">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-primary"
|
|
onclick="configManager.downloadConfig('${this.escapeHtml(config.filename)}')"
|
|
title="Download config">
|
|
<i class="bi bi-download"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger"
|
|
onclick="configManager.confirmDelete('${this.escapeHtml(config.filename)}', ${scheduleCount})"
|
|
title="Delete config"
|
|
${scheduleCount > 0 ? 'disabled' : ''}>
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
`;
|
|
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
// Update result count
|
|
this.updateResultCount(configs.length);
|
|
}
|
|
|
|
/**
|
|
* View/preview a configuration file
|
|
*/
|
|
async viewConfig(filename) {
|
|
try {
|
|
const config = await this.getConfig(filename);
|
|
|
|
// Show modal with config content
|
|
const modalHtml = `
|
|
<div class="modal fade" id="viewConfigModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">${this.escapeHtml(filename)}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<pre><code class="language-yaml">${this.escapeHtml(config.content)}</code></pre>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-primary"
|
|
onclick="configManager.downloadConfig('${this.escapeHtml(filename)}')">
|
|
<i class="bi bi-download"></i> Download
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Remove existing modal if any
|
|
const existingModal = document.getElementById('viewConfigModal');
|
|
if (existingModal) {
|
|
existingModal.remove();
|
|
}
|
|
|
|
// Add modal to page
|
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
|
|
// Show modal
|
|
const modal = new bootstrap.Modal(document.getElementById('viewConfigModal'));
|
|
modal.show();
|
|
|
|
// Clean up on close
|
|
document.getElementById('viewConfigModal').addEventListener('hidden.bs.modal', function() {
|
|
this.remove();
|
|
});
|
|
|
|
} catch (error) {
|
|
this.showError('Failed to view configuration: ' + error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Confirm deletion of a configuration
|
|
*/
|
|
confirmDelete(filename, scheduleCount) {
|
|
if (scheduleCount > 0) {
|
|
this.showError(`Cannot delete "${filename}" - it is used by ${scheduleCount} schedule(s)`);
|
|
return;
|
|
}
|
|
|
|
if (confirm(`Are you sure you want to delete "${filename}"?\n\nThis action cannot be undone.`)) {
|
|
this.performDelete(filename);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Perform the actual deletion
|
|
*/
|
|
async performDelete(filename) {
|
|
try {
|
|
await this.deleteConfig(filename);
|
|
this.showSuccess(`Configuration "${filename}" deleted successfully`);
|
|
|
|
// Reload configs table
|
|
await this.loadConfigs();
|
|
} catch (error) {
|
|
this.showError('Failed to delete configuration: ' + error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Filter configs table by search term
|
|
*/
|
|
filterConfigs(searchTerm) {
|
|
const term = searchTerm.toLowerCase().trim();
|
|
const rows = document.querySelectorAll('#configs-table tbody tr');
|
|
let visibleCount = 0;
|
|
|
|
rows.forEach(row => {
|
|
// Skip empty state row
|
|
if (row.querySelector('td[colspan]')) {
|
|
return;
|
|
}
|
|
|
|
const filename = row.cells[0]?.textContent.toLowerCase() || '';
|
|
const title = row.cells[1]?.textContent.toLowerCase() || '';
|
|
|
|
const matches = filename.includes(term) || title.includes(term);
|
|
|
|
row.style.display = matches ? '' : 'none';
|
|
if (matches) visibleCount++;
|
|
});
|
|
|
|
this.updateResultCount(visibleCount);
|
|
}
|
|
|
|
/**
|
|
* Update result count display
|
|
*/
|
|
updateResultCount(count) {
|
|
const countElement = document.getElementById('result-count');
|
|
if (countElement) {
|
|
countElement.textContent = `${count} config${count !== 1 ? 's' : ''}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show error message
|
|
*/
|
|
showError(message, elementId = 'error-display') {
|
|
const errorElement = document.getElementById(elementId);
|
|
if (errorElement) {
|
|
errorElement.innerHTML = `
|
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
<i class="bi bi-exclamation-triangle"></i> ${this.escapeHtml(message)}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
`;
|
|
errorElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
} else {
|
|
console.error('Error:', message);
|
|
alert('Error: ' + message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show success message
|
|
*/
|
|
showSuccess(message, elementId = 'success-display') {
|
|
const successElement = document.getElementById(elementId);
|
|
if (successElement) {
|
|
successElement.innerHTML = `
|
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
<i class="bi bi-check-circle"></i> ${this.escapeHtml(message)}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
`;
|
|
successElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
} else {
|
|
console.log('Success:', message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear all messages
|
|
*/
|
|
clearMessages() {
|
|
const elements = ['error-display', 'success-display', 'csv-errors', 'yaml-errors'];
|
|
elements.forEach(id => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.innerHTML = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Format file size for display
|
|
*/
|
|
formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
/**
|
|
* Escape HTML to prevent XSS
|
|
*/
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
// Initialize global config manager instance
|
|
const configManager = new ConfigManager();
|
|
|
|
/**
|
|
* Setup drag-and-drop zone for file uploads
|
|
*/
|
|
function setupDropzone(dropzoneId, fileInputId, fileType, onUploadCallback) {
|
|
const dropzone = document.getElementById(dropzoneId);
|
|
const fileInput = document.getElementById(fileInputId);
|
|
|
|
if (!dropzone || !fileInput) {
|
|
console.warn(`Dropzone setup failed: missing elements (${dropzoneId}, ${fileInputId})`);
|
|
return;
|
|
}
|
|
|
|
// Click to browse
|
|
dropzone.addEventListener('click', () => {
|
|
fileInput.click();
|
|
});
|
|
|
|
// Drag over
|
|
dropzone.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dropzone.classList.add('dragover');
|
|
});
|
|
|
|
// Drag leave
|
|
dropzone.addEventListener('dragleave', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dropzone.classList.remove('dragover');
|
|
});
|
|
|
|
// Drop
|
|
dropzone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dropzone.classList.remove('dragover');
|
|
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
handleFileUpload(files[0], fileType, onUploadCallback);
|
|
}
|
|
});
|
|
|
|
// File input change
|
|
fileInput.addEventListener('change', (e) => {
|
|
const files = e.target.files;
|
|
if (files.length > 0) {
|
|
handleFileUpload(files[0], fileType, onUploadCallback);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle file upload (CSV or YAML)
|
|
*/
|
|
async function handleFileUpload(file, fileType, callback) {
|
|
configManager.clearMessages();
|
|
|
|
// Validate file type
|
|
const extension = file.name.split('.').pop().toLowerCase();
|
|
|
|
if (fileType === 'csv' && extension !== 'csv') {
|
|
configManager.showError('Please upload a CSV file (.csv)', 'csv-errors');
|
|
return;
|
|
}
|
|
|
|
if (fileType === 'yaml' && !['yaml', 'yml'].includes(extension)) {
|
|
configManager.showError('Please upload a YAML file (.yaml or .yml)', 'yaml-errors');
|
|
return;
|
|
}
|
|
|
|
// Validate file size (2MB limit for configs)
|
|
const maxSize = 2 * 1024 * 1024; // 2MB
|
|
if (file.size > maxSize) {
|
|
const errorId = fileType === 'csv' ? 'csv-errors' : 'yaml-errors';
|
|
configManager.showError(`File too large (${configManager.formatFileSize(file.size)}). Maximum size is 2MB.`, errorId);
|
|
return;
|
|
}
|
|
|
|
// Call the provided callback
|
|
if (callback) {
|
|
try {
|
|
await callback(file);
|
|
} catch (error) {
|
|
const errorId = fileType === 'csv' ? 'csv-errors' : 'yaml-errors';
|
|
configManager.showError(error.message, errorId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle CSV upload and preview
|
|
*/
|
|
async function handleCSVUpload(file) {
|
|
try {
|
|
// Show loading state
|
|
const previewPlaceholder = document.getElementById('preview-placeholder');
|
|
if (previewPlaceholder) {
|
|
previewPlaceholder.innerHTML = '<div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div>';
|
|
}
|
|
|
|
// Upload CSV
|
|
const result = await configManager.uploadCSV(file);
|
|
|
|
// Show preview
|
|
configManager.showPreview(result.preview, result.filename);
|
|
|
|
// Show success message
|
|
configManager.showSuccess(`CSV uploaded successfully! Preview the generated YAML below.`, 'csv-errors');
|
|
|
|
} catch (error) {
|
|
configManager.hidePreview();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle YAML upload
|
|
*/
|
|
async function handleYAMLUpload(file) {
|
|
try {
|
|
// Upload YAML
|
|
const result = await configManager.uploadYAML(file);
|
|
|
|
// Show success and redirect
|
|
configManager.showSuccess(`Configuration "${result.filename}" uploaded successfully!`, 'yaml-errors');
|
|
|
|
// Redirect to configs list after 2 seconds
|
|
setTimeout(() => {
|
|
window.location.href = '/configs';
|
|
}, 2000);
|
|
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save the previewed configuration (after CSV upload)
|
|
*/
|
|
async function savePreviewedConfig() {
|
|
if (!configManager.currentPreview || !configManager.currentFilename) {
|
|
configManager.showError('No configuration to save', 'csv-errors');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// The config is already saved during CSV upload, just redirect
|
|
configManager.showSuccess(`Configuration "${configManager.currentFilename}" saved successfully!`, 'csv-errors');
|
|
|
|
// Redirect to configs list after 2 seconds
|
|
setTimeout(() => {
|
|
window.location.href = '/configs';
|
|
}, 2000);
|
|
|
|
} catch (error) {
|
|
configManager.showError('Failed to save configuration: ' + error.message, 'csv-errors');
|
|
}
|
|
}
|