/** * 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 = `

No configuration files found. Create your first config!

`; 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 ? `${scheduleCount}` : 'None'; row.innerHTML = ` ${this.escapeHtml(config.filename)} ${this.escapeHtml(config.title || 'Untitled')} ${createdDate} ${fileSize} ${scheduleBadge} `; 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 = ` `; // 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 = ` `; 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 = ` `; 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 = '
Loading...
'; } // 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'); } }