diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-09-28 16:25:48 +0300 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-09-28 16:25:48 +0300 |
| commit | 38ac4db03560200df2517ed7e06e555828ce0a15 (patch) | |
| tree | 7bd8cc7d08d352bea05490e81b230a9d418a8553 /static/app.js | |
| parent | 8489f1f83da5dcc5818401393b1f6a430eea677c (diff) | |
| download | network-38ac4db03560200df2517ed7e06e555828ce0a15.tar.zst | |
Update
Diffstat (limited to 'static/app.js')
| -rw-r--r-- | static/app.js | 1185 |
1 files changed, 636 insertions, 549 deletions
diff --git a/static/app.js b/static/app.js index 22e52b5..3fdd32e 100644 --- a/static/app.js +++ b/static/app.js @@ -1,61 +1,65 @@ /* 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'; +import { ApiClient } from "./api-client.js"; +import { ConfigManager } from "./config-manager.js"; +import { EditorMode, 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 = ` + #elements; + #state; + #themeManager; + #structuredEditor; + + /** + * @param {Object} elements - DOM elements + */ + constructor(elements) { + this.#elements = elements; + this.#state = { + currentInterface: null, + interfaces: [], + editorMode: EditorMode.RAW, + currentConfigFile: null, + }; + + // 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 + * @private + */ + #createEditorModeToggle() { + const editorToggleHTML = ` <div class="editor-mode-toggle" style="margin-bottom: var(--spacing-l);"> - <button class="button small ${this.state.editorMode === EditorMode.RAW ? 'active' : ''}" + <button class="button small ${this.#state.editorMode === EditorMode.RAW ? "active" : ""}" data-mode="raw" id="rawEditorBtn"> 📝 Raw Editor </button> - <button class="button small ${this.state.editorMode === EditorMode.STRUCTURED ? 'active' : ''}" + <button class="button small ${this.#state.editorMode === EditorMode.STRUCTURED ? "active" : ""}" data-mode="structured" id="structuredEditorBtn"> 🏗️ Structured Editor </button> @@ -68,512 +72,595 @@ class Application { </div> `; - // 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 = - `<div class="error-message">Error loading status: ${error.message}</div>`; - } - } - - /** - * 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}`; - } - } + // 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 + * @private + */ + #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 + * @private + */ + #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 + * @private + * @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 + * @private + */ + 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 + * @private + * @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 + * @private + */ + 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 + * @private + */ + 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 + * @private + */ + 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 + * @private + * @param {Object} detail - Event detail + */ + #handleAddSection(detail) { + console.log("Add section:", detail); + // TODO: Implement section addition logic + } + + /** + * Handle remove section from structured editor + * @private + * @param {Object} detail - Event detail + */ + #handleRemoveSection(detail) { + console.log("Remove section:", detail); + // TODO: Implement section removal logic + } + + /** + * Handle validate configuration + * @private + */ + 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 + * @private + * @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 + * @private + */ + #clearValidationResult() { + this.#elements.outputs.validateResult.textContent = ""; + this.#elements.outputs.validateResult.className = ""; + } + + /** + * Set editor mode (raw or structured) + * @private + * @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 + * @private + */ + 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}`); + } + } + + /** + * Load and display network status + * @private + */ + 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 = `<div class="error-message">Error loading status: ${error.message}</div>`; + } + } + + /** + * Show interface details + * @private + * @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 + * @private + */ + 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 + * @private + */ + 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 + * @private + */ + 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}`; + } + } + + /** + * 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]?.(); + } + + /** + * Get current application state (for debugging) + * @method getState + * @returns {Object} Application state + */ + getState() { + return { ...this.#state }; + } + + /** + * Get theme manager instance + * @method getThemeManager + * @returns {ThemeManager} Theme manager instance + */ + getThemeManager() { + return this.#themeManager; + } } // 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; +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 }; |
