summaryrefslogtreecommitdiffstats
path: root/static/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'static/app.js')
-rw-r--r--static/app.js605
1 files changed, 385 insertions, 220 deletions
diff --git a/static/app.js b/static/app.js
index 8a04c45..ca205f3 100644
--- a/static/app.js
+++ b/static/app.js
@@ -1,234 +1,399 @@
/* jshint esversion: 2024, module: true */
-import { ApiClient } from "./api-client.js";
-import { ConfigManager } from "./config-manager.js";
-import { InterfaceRenderer } from "./interface-renderer.js";
-import { ThemeManager } from "./theme-manager.js";
+import { ThemeManager } from './theme-manager.js';
+import { ApiClient } from './api-client.js';
+import { InterfaceRenderer } from './interface-renderer.js';
+import { ConfigManager } from './config-manager.js';
+import { StructuredEditor } from './structured-editor.js';
/**
* Main Application Class
* @class Application
*/
class Application {
- /**
- * @param {Object} elements - DOM elements
- */
- constructor(elements) {
- this.elements = elements;
- this.state = {
- currentInterface: null,
- interfaces: [],
- editorMode: "raw",
- };
-
- // Initialize modules
- this.themeManager = new ThemeManager(elements);
- this.apiClient = new ApiClient();
- this.interfaceRenderer = new InterfaceRenderer(elements, this.state);
- this.configManager = new ConfigManager(
- elements,
- this.apiClient,
- this.state,
- );
- }
-
- /**
- * Initialize the application
- * @method init
- */
- init() {
- this.themeManager.init();
- this.setupEventListeners();
- this.loadStatus();
- }
-
- /**
- * 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 - delegated to ConfigManager
- this.configManager.setupEventListeners();
-
- // 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);
- }
- };
-
- /**
- * 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) =>
- p?.classList.remove("active"),
- );
- this.elements.buttons.nav.forEach((btn) => btn?.classList.remove("active"));
-
- // Show selected panel and activate button
- this.elements.panels[panel]?.classList.add("active");
- document.querySelector(`[data-panel="${panel}"]`)?.classList.add("active");
-
- // Load panel-specific data
- const panelActions = {
- status: () => this.loadStatus(),
- configs: () => this.configManager.refreshConfigs(),
- logs: () => this.loadLogs(),
- };
-
- panelActions[panel]?.();
- }
-
- /**
- * Load and display network status
- * @method loadStatus
- */
- async loadStatus() {
- try {
- const data = await this.apiClient.get("/api/status");
- this.state.interfaces = data.Interfaces ?? [];
- this.interfaceRenderer.renderInterfaceTabs(this.state.interfaces);
-
- // Show first interface by default
- if (this.state.interfaces.length > 0 && !this.state.currentInterface) {
- this.interfaceRenderer.showInterfaceDetails(this.state.interfaces[0]);
- }
- } catch (error) {
- this.elements.outputs.ifaceDetails.innerHTML = `<div class="error-message">Error loading status: ${error.message}</div>`;
- }
- }
-
- /**
- * Load system logs
- * @method loadLogs
- */
- async loadLogs() {
- try {
- const text = await this.apiClient.getText("/api/logs");
- 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 this.apiClient.post("/api/reload");
- 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 this.apiClient.post("/api/reboot");
- this.elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`;
- } catch (error) {
- this.elements.outputs.cmdResult.textContent = `Error: ${error.message}`;
- }
- }
+ /**
+ * @param {Object} elements - DOM elements
+ */
+ constructor(elements) {
+ this.elements = elements;
+ this.state = {
+ currentInterface: null,
+ interfaces: [],
+ editorMode: 'raw', // 'raw' or 'structured'
+ currentConfigFile: null
+ };
+
+ // Initialize modules
+ this.themeManager = new ThemeManager(elements);
+ this.apiClient = new ApiClient();
+ this.interfaceRenderer = new InterfaceRenderer(elements, this.state);
+ this.configManager = new ConfigManager(elements, this.apiClient, this.state);
+
+ // Structured editor will be initialized after DOM is ready
+ this.structuredEditor = null;
+
+ // Create editor mode toggle UI
+ this.createEditorModeToggle();
+ }
+
+ /**
+ * Initialize the application
+ * @method init
+ */
+ init() {
+ this.themeManager.init();
+ this.setupEventListeners();
+ this.loadStatus();
+
+ // Initialize structured editor now that DOM is ready
+ this.initializeStructuredEditor();
+ }
+
+ /**
+ * Create editor mode toggle UI
+ * @method createEditorModeToggle
+ */
+ createEditorModeToggle() {
+ const editorToggleHTML = `
+ <div class="editor-mode-toggle" style="margin-bottom: var(--spacing-l);">
+ <button class="button small ${this.state.editorMode === 'raw' ? 'active' : ''}"
+ data-mode="raw" id="rawEditorBtn">
+ 📝 Raw Editor
+ </button>
+ <button class="button small ${this.state.editorMode === 'structured' ? 'active' : ''}"
+ data-mode="structured" id="structuredEditorBtn">
+ 🏗️ Structured Editor
+ </button>
+ </div>
+ <div id="rawEditorContainer">
+ <!-- Existing raw editor will go here -->
+ </div>
+ <div id="structuredEditorContainer" style="display: none;">
+ <!-- Structured editor will be rendered here -->
+ </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.configManager.refreshConfigs());
+ this.elements.buttons.saveConfig?.addEventListener('click',
+ () => this.handleSaveConfig());
+ this.elements.buttons.validateConfig?.addEventListener('click',
+ () => this.configManager.validateConfig());
+ this.elements.inputs.configSelect?.addEventListener('change',
+ () => this.handleConfigFileChange());
+
+ // Editor mode toggle
+ this.elements.editorButtons?.raw?.addEventListener('click',
+ () => this.setEditorMode('raw'));
+ this.elements.editorButtons?.structured?.addEventListener('click',
+ () => this.setEditorMode('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 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 === 'raw') {
+ await this.configManager.loadConfig();
+ } else {
+ await this.loadConfigForStructuredEditor();
+ }
+ }
+
+ /**
+ * Load config for structured editor
+ * @method loadConfigForStructuredEditor
+ */
+ async loadConfigForStructuredEditor() {
+ if (!this.structuredEditor) {
+ console.error('Structured editor not initialized');
+ return;
+ }
+
+ try {
+ const text = await this.apiClient.getText(`/api/config/${encodeURIComponent(this.state.currentConfigFile)}`);
+ await this.structuredEditor.loadConfiguration(text, this.state.currentConfigFile);
+ } catch (error) {
+ alert(`Failed to load config for structured editor: ${error.message}`);
+ }
+ }
+
+ /**
+ * Set editor mode (raw or structured)
+ * @method setEditorMode
+ * @param {string} 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 === 'raw');
+ this.elements.editorButtons.structured.classList.toggle('active', mode === 'structured');
+ }
+
+ if (this.elements.editorContainers?.raw && this.elements.editorContainers?.structured) {
+ this.elements.editorContainers.raw.style.display = mode === 'raw' ? 'block' : 'none';
+ this.elements.editorContainers.structured.style.display = mode === 'structured' ? 'block' : 'none';
+ }
+
+ // If switching to structured mode and we have a config file loaded, load it
+ if (mode === '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 === '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 this.apiClient.post('/api/save', { name, content, restart });
+ alert(`Saved: ${result.status ?? 'ok'}`);
+
+ // Refresh the config in structured editor if needed
+ if (this.state.editorMode === 'structured' && this.structuredEditor) {
+ await this.structuredEditor.loadConfiguration(content, name);
+ }
+ } 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.configManager.refreshConfigs(),
+ logs: () => this.loadLogs(),
+ };
+
+ panelActions[panel]?.();
+ }
+
+ /**
+ * Load and display network status
+ * @method loadStatus
+ */
+ async loadStatus() {
+ try {
+ const data = await this.apiClient.get('/api/status');
+ this.state.interfaces = data.Interfaces ?? [];
+ this.interfaceRenderer.renderInterfaceTabs(this.state.interfaces);
+
+ // Show first interface by default
+ if (this.state.interfaces.length > 0 && !this.state.currentInterface) {
+ this.interfaceRenderer.showInterfaceDetails(this.state.interfaces[0]);
+ }
+ } catch (error) {
+ this.elements.outputs.ifaceDetails.innerHTML =
+ `<div class="error-message">Error loading status: ${error.message}</div>`;
+ }
+ }
+
+ /**
+ * Load system logs
+ * @method loadLogs
+ */
+ async loadLogs() {
+ try {
+ const text = await this.apiClient.getText('/api/logs');
+ 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 this.apiClient.post('/api/reload');
+ 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 this.apiClient.post('/api/reboot');
+ 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;
+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 };