/* jshint esversion: 2024, module: true */
import { ApiClient } from './api-client.js';
import { ConfigManager } from './config-manager.js';
import { EditorMode, ThemeMode, ValidationState } from './enums.js';
import { InterfaceRenderer } from './interface-renderer.js';
import { StructuredEditor } from './structured-editor.js';
import { ThemeManager } from './theme-manager.js';
/**
* Main Application Class
* @class Application
*/
class Application {
/**
* @param {Object} elements - DOM elements
*/
constructor(elements) {
this.elements = elements;
this.state = {
currentInterface: null,
interfaces: [],
editorMode: EditorMode.STRUCTURED,
currentConfigFile: null,
theme: ThemeMode.DARK
};
// Initialize modules
this.themeManager = new ThemeManager(elements);
this.structuredEditor = null;
// Create editor mode toggle UI
this.createEditorModeToggle();
}
/**
* Initialize the application
* @method init
*/
init() {
this.themeManager.init();
this.setupEventListeners();
this.loadStatus();
this.initializeStructuredEditor();
}
/**
* Create editor mode toggle UI
* @method createEditorModeToggle
*/
createEditorModeToggle() {
const editorToggleHTML = `
`;
// Insert the toggle and containers into the configs panel
const configCard = this.elements.panels.configs.querySelector('.card:last-child');
configCard.insertAdjacentHTML('afterbegin', editorToggleHTML);
// Move existing form elements to raw editor container
const rawEditorContainer = document.getElementById('rawEditorContainer');
const formGroups = Array.from(configCard.querySelectorAll('.form-group, .checkbox-group'));
const configActions = configCard.querySelector('.config-actions') ||
configCard.querySelector('div:has(> #validateConfig)');
formGroups.forEach(group => {
if (!group.closest('.editor-mode-toggle')) {
rawEditorContainer.appendChild(group);
}
});
// Move config actions if they exist
if (configActions) {
rawEditorContainer.appendChild(configActions);
}
// Update elements reference
this.elements.editorContainers = {
raw: document.getElementById('rawEditorContainer'),
structured: document.getElementById('structuredEditorContainer')
};
this.elements.editorButtons = {
raw: document.getElementById('rawEditorBtn'),
structured: document.getElementById('structuredEditorBtn')
};
}
/**
* Initialize structured editor
* @method initializeStructuredEditor
*/
initializeStructuredEditor() {
if (this.elements.editorContainers.structured) {
this.structuredEditor = new StructuredEditor(this.elements.editorContainers.structured);
} else {
console.error('Structured editor container not found');
}
}
/**
* Set up all event listeners
* @method setupEventListeners
*/
setupEventListeners() {
// Navigation
this.elements.buttons.nav.forEach(button => {
button.addEventListener('click', (event) => {
this.show(event.currentTarget.dataset.panel);
});
});
// Status panel
this.elements.buttons.refreshStatus?.addEventListener('click', () => this.loadStatus());
// Configs panel - raw editor
this.elements.buttons.refreshConfigs?.addEventListener('click',
() => this.handleRefreshConfigs());
this.elements.buttons.saveConfig?.addEventListener('click',
() => this.handleSaveConfig());
this.elements.buttons.validateConfig?.addEventListener('click',
() => this.handleValidateConfig());
this.elements.inputs.configSelect?.addEventListener('change',
() => this.handleConfigFileChange());
// Editor mode toggle
this.elements.editorButtons?.raw?.addEventListener('click',
() => this.setEditorMode(EditorMode.RAW));
this.elements.editorButtons?.structured?.addEventListener('click',
() => this.setEditorMode(EditorMode.STRUCTURED));
// Logs panel
this.elements.buttons.refreshLogs?.addEventListener('click', () => this.loadLogs());
// Commands panel
this.elements.buttons.restartNetworkd?.addEventListener('click', () => this.restartNetworkd());
this.elements.buttons.rebootDevice?.addEventListener('click', () => this.rebootDevice());
// Touch support
document.addEventListener('touchstart', this.handleTouchStart, { passive: true });
}
/**
* Handle touch events for better mobile support
* @method handleTouchStart
* @param {TouchEvent} event
*/
handleTouchStart = (event) => {
// Add visual feedback for touch
if (event.target.classList.contains('button') || event.target.classList.contains('nav-button')) {
event.target.style.opacity = '0.7';
setTimeout(() => {
event.target.style.opacity = '';
}, 150);
}
};
/**
* Handle refresh configuration files
* @method handleRefreshConfigs
*/
async handleRefreshConfigs() {
try {
const files = await ConfigManager.refreshConfigs();
this.updateConfigSelect(files);
if (files.length > 0) {
this.state.currentConfigFile = files[0];
await this.handleConfigFileChange();
} else {
this.elements.inputs.cfgEditor.value = '';
this.state.currentConfigFile = null;
}
} catch (error) {
alert(error.message);
}
}
/**
* Update configuration select element
* @method updateConfigSelect
* @param {Array} files - File names
*/
updateConfigSelect(files) {
this.elements.inputs.configSelect.innerHTML = '';
files.forEach(file => {
const option = new Option(file, file);
this.elements.inputs.configSelect.add(option);
});
}
/**
* Handle configuration file change
* @method handleConfigFileChange
*/
async handleConfigFileChange() {
const name = this.elements.inputs.configSelect.value;
if (!name) return;
this.state.currentConfigFile = name;
if (this.state.editorMode === EditorMode.RAW) {
await this.loadConfigForRawEditor();
} else {
await this.loadConfigForStructuredEditor();
}
}
/**
* Load config for raw editor
* @method loadConfigForRawEditor
*/
async loadConfigForRawEditor() {
try {
const content = await ConfigManager.loadConfig(this.state.currentConfigFile);
this.elements.inputs.cfgEditor.value = content;
this.clearValidationResult();
} catch (error) {
alert(error.message);
}
}
/**
* Load config for structured editor
* @method loadConfigForStructuredEditor
*/
async loadConfigForStructuredEditor() {
if (!this.structuredEditor) {
console.error('Structured editor not initialized');
return;
}
try {
const content = await ConfigManager.loadConfig(this.state.currentConfigFile);
// Parse configuration and get schema
const { NetworkConfiguration } = await import('./systemd-network.js');
const config = NetworkConfiguration.fromSystemdConfiguration(content);
const schema = config.getSchema();
// Load schema into structured editor
this.structuredEditor.loadSchema(schema, this.state.currentConfigFile);
// Set up event listeners for structured editor
this.structuredEditor.on('addSection', (event) => this.handleAddSection(event.detail));
this.structuredEditor.on('removeSection', (event) => this.handleRemoveSection(event.detail));
} catch (error) {
alert(`Failed to load config for structured editor: ${error.message}`);
}
}
/**
* Handle add section from structured editor
* @method handleAddSection
* @param {Object} detail - Event detail
*/
handleAddSection(detail) {
console.log('Add section:', detail);
// TODO: Implement section addition logic
// This would involve updating the schema and re-rendering
}
/**
* Handle remove section from structured editor
* @method handleRemoveSection
* @param {Object} detail - Event detail
*/
handleRemoveSection(detail) {
console.log('Remove section:', detail);
// TODO: Implement section removal logic
// This would involve updating the schema and re-rendering
}
/**
* Handle validate configuration
* @method handleValidateConfig
*/
async handleValidateConfig() {
const name = this.state.currentConfigFile;
if (!name) {
alert('Please select a configuration file first.');
return;
}
let content;
if (this.state.editorMode === EditorMode.RAW) {
content = this.elements.inputs.cfgEditor.value;
} else if (this.structuredEditor) {
content = this.structuredEditor.getConfigurationText();
} else {
alert('Structured editor not available');
return;
}
this.setValidationResult(ValidationState.PENDING);
try {
const result = await ConfigManager.validateConfig(name, content);
if (result.ok) {
this.setValidationResult(ValidationState.SUCCESS);
} else {
this.setValidationResult(ValidationState.ERROR, result.output);
}
} catch (error) {
this.setValidationResult(ValidationState.ERROR, error.message);
}
}
/**
* Set validation result
* @method setValidationResult
* @param {Symbol} state - Validation state
* @param {string} [message] - Additional message
*/
setValidationResult(state, message = '') {
this.elements.outputs.validateResult.textContent = ConfigManager.getValidationMessage(state, message);
this.elements.outputs.validateResult.className = ConfigManager.getValidationClass(state);
}
/**
* Clear validation result
* @method clearValidationResult
*/
clearValidationResult() {
this.elements.outputs.validateResult.textContent = '';
this.elements.outputs.validateResult.className = '';
}
/**
* Set editor mode (raw or structured)
* @method setEditorMode
* @param {Symbol} mode - Editor mode
*/
setEditorMode(mode) {
this.state.editorMode = mode;
// Update UI
if (this.elements.editorButtons?.raw && this.elements.editorButtons?.structured) {
this.elements.editorButtons.raw.classList.toggle('active', mode === EditorMode.RAW);
this.elements.editorButtons.structured.classList.toggle('active', mode === EditorMode.STRUCTURED);
}
if (this.elements.editorContainers?.raw && this.elements.editorContainers?.structured) {
this.elements.editorContainers.raw.style.display = mode === EditorMode.RAW ? 'block' : 'none';
this.elements.editorContainers.structured.style.display = mode === EditorMode.STRUCTURED ? 'block' : 'none';
}
// If switching to structured mode and we have a config file loaded, load it
if (mode === EditorMode.STRUCTURED && this.state.currentConfigFile && this.structuredEditor) {
this.loadConfigForStructuredEditor();
}
}
/**
* Handle save configuration based on current editor mode
* @method handleSaveConfig
*/
async handleSaveConfig() {
const name = this.state.currentConfigFile;
if (!name) {
alert('Please select a configuration file first.');
return;
}
const restart = this.elements.inputs.restartAfterSave.checked;
if (!confirm(`Save file ${name}? This will create a backup and ${restart ? 'restart' : 'not restart'} networkd.`)) {
return;
}
try {
let content;
if (this.state.editorMode === EditorMode.RAW) {
content = this.elements.inputs.cfgEditor.value;
} else if (this.structuredEditor) {
content = this.structuredEditor.getConfigurationText();
} else {
throw new Error('Structured editor not available');
}
const result = await ConfigManager.saveConfig(name, content, restart);
alert(`Saved: ${result.status ?? 'ok'}`);
// Refresh the config in structured editor if needed
if (this.state.editorMode === EditorMode.STRUCTURED && this.structuredEditor) {
await this.loadConfigForStructuredEditor();
}
} catch (error) {
alert(`Save failed: ${error.message}`);
}
}
/**
* Show specified panel and hide others
* @method show
* @param {string} panel - Panel to show
*/
show(panel) {
// Hide all panels and remove active class from buttons
Object.values(this.elements.panels).forEach(p => {
if (p) p.classList.remove('active');
});
this.elements.buttons.nav.forEach(btn => {
if (btn) btn.classList.remove('active');
});
// Show selected panel and activate button
const targetPanel = this.elements.panels[panel];
const targetButton = document.querySelector(`[data-panel="${panel}"]`);
if (targetPanel) targetPanel.classList.add('active');
if (targetButton) targetButton.classList.add('active');
// Load panel-specific data
const panelActions = {
status: () => this.loadStatus(),
configs: () => this.handleRefreshConfigs(),
logs: () => this.loadLogs(),
};
panelActions[panel]?.();
}
/**
* Load and display network status
* @method loadStatus
*/
async loadStatus() {
try {
const data = await ApiClient.getNetworkStatus();
this.state.interfaces = data.Interfaces ?? [];
// Render interface tabs
const tabsHTML = InterfaceRenderer.renderInterfaceTabs(this.state.interfaces, this.state.currentInterface);
this.elements.outputs.ifaceTabs.innerHTML = tabsHTML;
// Show first interface by default
if (this.state.interfaces.length > 0 && !this.state.currentInterface) {
this.showInterfaceDetails(this.state.interfaces[0]);
}
// Add event listeners to tabs
this.elements.outputs.ifaceTabs.querySelectorAll('.interface-tab').forEach(tab => {
tab.addEventListener('click', (event) => {
const ifaceName = event.currentTarget.dataset.interface;
const iface = this.state.interfaces.find(i => i.Name === ifaceName);
if (iface) {
this.showInterfaceDetails(iface);
}
});
});
} catch (error) {
this.elements.outputs.ifaceDetails.innerHTML =
`Error loading status: ${error.message}
`;
}
}
/**
* Show interface details
* @method showInterfaceDetails
* @param {Object} iface - Interface object
*/
showInterfaceDetails(iface) {
this.state.currentInterface = iface;
// Update active tab
this.elements.outputs.ifaceTabs.querySelectorAll('.interface-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.interface === iface.Name);
});
const detailsHTML = InterfaceRenderer.showInterfaceDetails(iface);
this.elements.outputs.ifaceDetails.innerHTML = detailsHTML;
}
/**
* Load system logs
* @method loadLogs
*/
async loadLogs() {
try {
const text = await ApiClient.getSystemLogs();
this.elements.outputs.logsArea.textContent = text;
} catch (error) {
this.elements.outputs.logsArea.textContent = `Error: ${error.message}`;
}
}
/**
* Restart networkd service
* @method restartNetworkd
*/
async restartNetworkd() {
if (!confirm('Restart systemd-networkd? Active connections may be reset.')) return;
try {
const result = await ApiClient.restartNetworkd();
this.elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`;
} catch (error) {
this.elements.outputs.cmdResult.textContent = `Error: ${error.message}`;
}
}
/**
* Reboot the device
* @method rebootDevice
*/
async rebootDevice() {
if (!confirm('Reboot device now?')) return;
try {
const result = await ApiClient.rebootDevice();
this.elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`;
} catch (error) {
this.elements.outputs.cmdResult.textContent = `Error: ${error.message}`;
}
}
}
// Initialize application when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
const elements = {
themeToggle: document.getElementById('themeToggle'),
themeIcon: document.getElementById('themeIcon'),
panels: {
status: document.getElementById('panelStatus'),
configs: document.getElementById('panelConfigs'),
logs: document.getElementById('panelLogs'),
commands: document.getElementById('panelCommands'),
},
buttons: {
nav: document.querySelectorAll('.nav-button'),
refreshStatus: document.getElementById('refreshStatus'),
refreshConfigs: document.getElementById('refreshConfigs'),
saveConfig: document.getElementById('saveConfig'),
validateConfig: document.getElementById('validateConfig'),
refreshLogs: document.getElementById('refreshLogs'),
restartNetworkd: document.getElementById('restartNetworkd'),
rebootDevice: document.getElementById('rebootDevice'),
},
inputs: {
configSelect: document.getElementById('configSelect'),
cfgEditor: document.getElementById('cfgEditor'),
restartAfterSave: document.getElementById('restartAfterSave'),
},
outputs: {
ifaceTabs: document.getElementById('interfaceTabs'),
ifaceDetails: document.getElementById('interfaceDetails'),
validateResult: document.getElementById('validateResult'),
logsArea: document.getElementById('logsArea'),
cmdResult: document.getElementById('cmdResult'),
},
};
const app = new Application(elements);
app.init();
// Make app globally available for debugging
window.app = app;
});
export { Application };