Fix contrast issues with text-muted and bg-dark classes
- Fixed Bootstrap bg-dark class to use better contrasting color - Added comprehensive text-muted contrast fixes for various contexts - Improved dark theme colors for better accessibility - Fixed CSS inheritance issues for code elements in dark contexts - All color choices meet WCAG AA contrast requirements
This commit is contained in:
567
frontend/static/js/editor.js
Normal file
567
frontend/static/js/editor.js
Normal file
@@ -0,0 +1,567 @@
|
||||
// UnitForge Editor JavaScript
|
||||
// Handles the unit file editor functionality
|
||||
|
||||
class UnitFileEditor {
|
||||
constructor() {
|
||||
this.currentUnitType = 'service';
|
||||
this.currentContent = '';
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.initializeEditor();
|
||||
this.updateFilename();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Unit type change
|
||||
const unitTypeSelect = document.getElementById('unitType');
|
||||
if (unitTypeSelect) {
|
||||
unitTypeSelect.addEventListener('change', this.changeUnitType.bind(this));
|
||||
}
|
||||
|
||||
// Unit name change
|
||||
const unitNameInput = document.getElementById('unitName');
|
||||
if (unitNameInput) {
|
||||
unitNameInput.addEventListener('input', this.updateFilename.bind(this));
|
||||
}
|
||||
|
||||
// Editor content change
|
||||
const editor = document.getElementById('editor');
|
||||
if (editor) {
|
||||
editor.addEventListener('input', this.debounce(this.onEditorChange.bind(this), 300));
|
||||
}
|
||||
|
||||
// File upload
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', this.handleFileUpload.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
initializeEditor() {
|
||||
this.generateBasicUnit();
|
||||
}
|
||||
|
||||
changeUnitType() {
|
||||
const unitType = document.getElementById('unitType').value;
|
||||
this.currentUnitType = unitType;
|
||||
|
||||
// Show/hide type-specific fields
|
||||
this.toggleTypeFields(unitType);
|
||||
|
||||
// Update filename
|
||||
this.updateFilename();
|
||||
|
||||
// Generate new basic unit
|
||||
this.generateBasicUnit();
|
||||
}
|
||||
|
||||
toggleTypeFields(unitType) {
|
||||
// Hide all type-specific fields
|
||||
const allFields = document.querySelectorAll('.unit-type-fields');
|
||||
allFields.forEach(field => field.classList.add('d-none'));
|
||||
|
||||
// Show relevant fields
|
||||
const targetFields = document.getElementById(`${unitType}Fields`);
|
||||
if (targetFields) {
|
||||
targetFields.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
updateFilename() {
|
||||
const unitName = document.getElementById('unitName').value || 'myservice';
|
||||
const unitType = document.getElementById('unitType').value;
|
||||
const filename = `${unitName}.${unitType}`;
|
||||
|
||||
const filenameElement = document.getElementById('filename');
|
||||
if (filenameElement) {
|
||||
filenameElement.textContent = filename;
|
||||
}
|
||||
}
|
||||
|
||||
generateBasicUnit() {
|
||||
const unitType = this.currentUnitType;
|
||||
const unitName = document.getElementById('unitName').value || 'myservice';
|
||||
const description = document.getElementById('unitDescription').value || 'My Service Description';
|
||||
|
||||
let content = `[Unit]\nDescription=${description}\n`;
|
||||
|
||||
// Add common dependencies
|
||||
if (unitType === 'service' || unitType === 'timer') {
|
||||
content += 'After=network.target\n';
|
||||
}
|
||||
|
||||
content += '\n';
|
||||
|
||||
// Add type-specific sections
|
||||
switch (unitType) {
|
||||
case 'service':
|
||||
content += '[Service]\n';
|
||||
content += 'Type=simple\n';
|
||||
content += 'ExecStart=/usr/bin/myapp\n';
|
||||
content += 'User=www-data\n';
|
||||
content += 'Restart=on-failure\n';
|
||||
break;
|
||||
|
||||
case 'timer':
|
||||
content += '[Timer]\n';
|
||||
content += 'OnCalendar=daily\n';
|
||||
content += 'Persistent=true\n';
|
||||
break;
|
||||
|
||||
case 'socket':
|
||||
content += '[Socket]\n';
|
||||
content += 'ListenStream=127.0.0.1:8080\n';
|
||||
content += 'SocketUser=www-data\n';
|
||||
break;
|
||||
|
||||
case 'mount':
|
||||
content += '[Mount]\n';
|
||||
content += 'What=/dev/disk/by-uuid/12345678-1234-1234-1234-123456789abc\n';
|
||||
content += 'Where=/mnt/mydisk\n';
|
||||
content += 'Type=ext4\n';
|
||||
content += 'Options=defaults\n';
|
||||
break;
|
||||
|
||||
case 'target':
|
||||
content += '[Unit]\n';
|
||||
content += 'Requires=multi-user.target\n';
|
||||
break;
|
||||
|
||||
case 'path':
|
||||
content += '[Path]\n';
|
||||
content += 'PathExists=/var/spool/myapp\n';
|
||||
content += 'Unit=myapp.service\n';
|
||||
break;
|
||||
}
|
||||
|
||||
// Add Install section for most types
|
||||
if (unitType !== 'target') {
|
||||
content += '\n[Install]\n';
|
||||
if (unitType === 'timer') {
|
||||
content += 'WantedBy=timers.target\n';
|
||||
} else if (unitType === 'socket') {
|
||||
content += 'WantedBy=sockets.target\n';
|
||||
} else {
|
||||
content += 'WantedBy=multi-user.target\n';
|
||||
}
|
||||
}
|
||||
|
||||
this.setEditorContent(content);
|
||||
}
|
||||
|
||||
setEditorContent(content) {
|
||||
const editor = document.getElementById('editor');
|
||||
if (editor) {
|
||||
editor.value = content;
|
||||
this.currentContent = content;
|
||||
this.updateUnitInfo();
|
||||
}
|
||||
}
|
||||
|
||||
onEditorChange() {
|
||||
const editor = document.getElementById('editor');
|
||||
if (editor) {
|
||||
this.currentContent = editor.value;
|
||||
this.updateUnitInfo();
|
||||
}
|
||||
}
|
||||
|
||||
updateUnitInfo() {
|
||||
// Update basic info display
|
||||
const lines = this.currentContent.split('\n');
|
||||
const sections = this.countSections(lines);
|
||||
|
||||
const infoType = document.getElementById('infoType');
|
||||
const infoSections = document.getElementById('infoSections');
|
||||
|
||||
if (infoType) {
|
||||
infoType.textContent = this.currentUnitType;
|
||||
}
|
||||
|
||||
if (infoSections) {
|
||||
infoSections.textContent = sections;
|
||||
}
|
||||
}
|
||||
|
||||
countSections(lines) {
|
||||
let count = 0;
|
||||
for (const line of lines) {
|
||||
if (line.trim().match(/^\[.+\]$/)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
updateField(section, key, value) {
|
||||
if (!value.trim()) return;
|
||||
|
||||
const lines = this.currentContent.split('\n');
|
||||
let newLines = [];
|
||||
let currentSection = '';
|
||||
let foundSection = false;
|
||||
let foundKey = false;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const sectionMatch = line.match(/^\[(.+)\]$/);
|
||||
|
||||
if (sectionMatch) {
|
||||
currentSection = sectionMatch[1];
|
||||
foundSection = (currentSection === section);
|
||||
newLines.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (foundSection && line.includes('=')) {
|
||||
const [lineKey] = line.split('=', 2);
|
||||
if (lineKey.trim() === key) {
|
||||
newLines.push(`${key}=${value}`);
|
||||
foundKey = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
newLines.push(line);
|
||||
}
|
||||
|
||||
// If section or key wasn't found, add them
|
||||
if (!foundKey) {
|
||||
this.addKeyToSection(newLines, section, key, value);
|
||||
}
|
||||
|
||||
this.setEditorContent(newLines.join('\n'));
|
||||
}
|
||||
|
||||
addKeyToSection(lines, section, key, value) {
|
||||
let sectionIndex = -1;
|
||||
let nextSectionIndex = -1;
|
||||
|
||||
// Find the target section
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const sectionMatch = lines[i].match(/^\[(.+)\]$/);
|
||||
if (sectionMatch) {
|
||||
if (sectionMatch[1] === section) {
|
||||
sectionIndex = i;
|
||||
} else if (sectionIndex !== -1 && nextSectionIndex === -1) {
|
||||
nextSectionIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sectionIndex === -1) {
|
||||
// Section doesn't exist, add it
|
||||
lines.push('');
|
||||
lines.push(`[${section}]`);
|
||||
lines.push(`${key}=${value}`);
|
||||
} else {
|
||||
// Section exists, add key
|
||||
const insertIndex = nextSectionIndex === -1 ? lines.length : nextSectionIndex;
|
||||
lines.splice(insertIndex, 0, `${key}=${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
async validateUnit() {
|
||||
const content = this.currentContent;
|
||||
const filename = document.getElementById('filename').textContent;
|
||||
|
||||
const resultsDiv = document.getElementById('validationResults');
|
||||
if (!resultsDiv) return;
|
||||
|
||||
// Show loading
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>
|
||||
Validating unit file...
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await unitforge.validateUnitFile(content, filename);
|
||||
resultsDiv.innerHTML = unitforge.formatValidationResults(result);
|
||||
|
||||
if (result.valid) {
|
||||
unitforge.showToast('Unit file is valid!', 'success');
|
||||
} else {
|
||||
unitforge.showToast(`Found ${result.errors.length} error(s)`, 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
Validation failed: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
unitforge.showToast('Validation failed', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
resetEditor() {
|
||||
// Reset form fields
|
||||
document.getElementById('unitName').value = '';
|
||||
document.getElementById('unitDescription').value = '';
|
||||
document.getElementById('unitType').value = 'service';
|
||||
|
||||
// Reset type-specific fields
|
||||
document.getElementById('execStart').value = '';
|
||||
document.getElementById('user').value = '';
|
||||
document.getElementById('workingDirectory').value = '';
|
||||
|
||||
// Clear validation results
|
||||
const resultsDiv = document.getElementById('validationResults');
|
||||
if (resultsDiv) {
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Click "Validate" to check your unit file.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Regenerate basic unit
|
||||
this.currentUnitType = 'service';
|
||||
this.toggleTypeFields('service');
|
||||
this.updateFilename();
|
||||
this.generateBasicUnit();
|
||||
|
||||
unitforge.showToast('Editor reset', 'info');
|
||||
}
|
||||
|
||||
formatUnit() {
|
||||
const lines = this.currentContent.split('\n');
|
||||
const formatted = [];
|
||||
let inSection = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed.match(/^\[.+\]$/)) {
|
||||
// Section header
|
||||
if (inSection) {
|
||||
formatted.push(''); // Add blank line before new section
|
||||
}
|
||||
formatted.push(trimmed);
|
||||
inSection = true;
|
||||
} else if (trimmed === '') {
|
||||
// Empty line - only add if not consecutive
|
||||
if (formatted.length > 0 && formatted[formatted.length - 1] !== '') {
|
||||
formatted.push('');
|
||||
}
|
||||
} else if (trimmed.includes('=')) {
|
||||
// Key-value pair
|
||||
const [key, ...valueParts] = trimmed.split('=');
|
||||
const value = valueParts.join('=');
|
||||
formatted.push(`${key.trim()}=${value.trim()}`);
|
||||
} else {
|
||||
// Other content
|
||||
formatted.push(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing empty lines
|
||||
while (formatted.length > 0 && formatted[formatted.length - 1] === '') {
|
||||
formatted.pop();
|
||||
}
|
||||
|
||||
this.setEditorContent(formatted.join('\n'));
|
||||
unitforge.showToast('Unit file formatted', 'success');
|
||||
}
|
||||
|
||||
async copyToClipboard() {
|
||||
await unitforge.copyToClipboard(this.currentContent);
|
||||
}
|
||||
|
||||
downloadFile() {
|
||||
const filename = document.getElementById('filename').textContent;
|
||||
unitforge.downloadTextFile(this.currentContent, filename);
|
||||
unitforge.showToast(`Downloaded ${filename}`, 'success');
|
||||
}
|
||||
|
||||
insertPattern(patternType) {
|
||||
let pattern = '';
|
||||
|
||||
switch (patternType) {
|
||||
case 'web-service':
|
||||
pattern = `[Unit]
|
||||
Description=Web Application Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/node /opt/webapp/server.js
|
||||
User=webapp
|
||||
Group=webapp
|
||||
WorkingDirectory=/opt/webapp
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PORT=3000
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target`;
|
||||
break;
|
||||
|
||||
case 'background-job':
|
||||
pattern = `[Unit]
|
||||
Description=Background Job Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/python3 /opt/app/worker.py
|
||||
User=worker
|
||||
Group=worker
|
||||
WorkingDirectory=/opt/app
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target`;
|
||||
break;
|
||||
|
||||
case 'database':
|
||||
pattern = `[Unit]
|
||||
Description=Database Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/mysqld --defaults-file=/etc/mysql/my.cnf
|
||||
User=mysql
|
||||
Group=mysql
|
||||
Restart=on-failure
|
||||
TimeoutSec=300
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (pattern) {
|
||||
this.setEditorContent(pattern);
|
||||
unitforge.showToast(`Inserted ${patternType} pattern`, 'success');
|
||||
}
|
||||
}
|
||||
|
||||
async handleFileUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const result = await unitforge.uploadUnitFile(file);
|
||||
|
||||
// Load content into editor
|
||||
this.setEditorContent(result.content);
|
||||
|
||||
// Update filename if available
|
||||
if (result.filename) {
|
||||
const nameWithoutExt = result.filename.replace(/\.[^/.]+$/, "");
|
||||
document.getElementById('unitName').value = nameWithoutExt;
|
||||
this.updateFilename();
|
||||
}
|
||||
|
||||
// Update unit type if detected
|
||||
if (result.unit_type) {
|
||||
document.getElementById('unitType').value = result.unit_type;
|
||||
this.currentUnitType = result.unit_type;
|
||||
this.toggleTypeFields(result.unit_type);
|
||||
}
|
||||
|
||||
// Show validation results
|
||||
const resultsDiv = document.getElementById('validationResults');
|
||||
if (resultsDiv && result.validation) {
|
||||
resultsDiv.innerHTML = unitforge.formatValidationResults(result.validation);
|
||||
}
|
||||
|
||||
// Close upload modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('uploadModal'));
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
|
||||
unitforge.showToast(`Loaded ${file.name}`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
unitforge.showToast(`Failed to load file: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Global functions for HTML onclick handlers
|
||||
function changeUnitType() {
|
||||
editor.changeUnitType();
|
||||
}
|
||||
|
||||
function updateFilename() {
|
||||
editor.updateFilename();
|
||||
}
|
||||
|
||||
function updateField(section, key, value) {
|
||||
editor.updateField(section, key, value);
|
||||
}
|
||||
|
||||
function validateUnit() {
|
||||
editor.validateUnit();
|
||||
}
|
||||
|
||||
function resetEditor() {
|
||||
editor.resetEditor();
|
||||
}
|
||||
|
||||
function formatUnit() {
|
||||
editor.formatUnit();
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
editor.copyToClipboard();
|
||||
}
|
||||
|
||||
function downloadFile() {
|
||||
editor.downloadFile();
|
||||
}
|
||||
|
||||
function insertPattern(patternType) {
|
||||
editor.insertPattern(patternType);
|
||||
}
|
||||
|
||||
function loadFile() {
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) {
|
||||
unitforge.showToast('Please select a file first', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger the file upload handling
|
||||
editor.handleFileUpload({ target: { files: [file] } });
|
||||
}
|
||||
|
||||
function showUploadModal() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('uploadModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Initialize the editor
|
||||
const editor = new UnitFileEditor();
|
||||
|
||||
// Export for use in other modules
|
||||
window.UnitFileEditor = UnitFileEditor;
|
||||
window.editor = editor;
|
||||
Reference in New Issue
Block a user