/* jshint esversion: 2024, module: true */ /** * Structured Editor for systemd-networkd configuration * @class StructuredEditor */ class StructuredEditor { constructor(container) { this.container = container; this.schema = null; this.currentFile = ""; } /** * Load configuration schema * @param {Object} schema - Configuration schema from NetworkConfiguration.getSchema() * @param {string} filename - File name */ loadSchema(schema, filename) { if (!this.container) { console.error("Structured editor container not found"); return; } try { this.schema = schema; this.currentFile = filename; this.render(); } catch (error) { console.error("Error loading schema:", error); this.showError(`Failed to load configuration schema: ${error.message}`); } } /** * Render the structured editor */ render() { if (!this.container) { console.error("Cannot render: container is null"); return; } if (!this.schema) { this.container.innerHTML = '
No configuration schema loaded
'; return; } try { this.container.innerHTML = this._createEditorHTML(); this._attachEventListeners(); } catch (error) { console.error("Error rendering structured editor:", error); this.showError(`Failed to render editor: ${error.message}`); } } /** * Create editor HTML structure from schema * @private * @returns {string} */ _createEditorHTML() { return `
${this._createSection("Match", this.schema.Match)} ${this._createSection("Link", this.schema.Link)} ${this._createSection("Network", this.schema.Network)} ${this._createSection("DHCP", this.schema.DHCP)} ${this._createArraySection("Address", this.schema.Address)} ${this._createArraySection("Route", this.schema.Route)}
`; } /** * Create a regular section * @private * @param {string} sectionName - Section name * @param {Object} sectionSchema - Section schema * @returns {string} */ _createSection(sectionName, sectionSchema) { if (!this._hasSectionValues(sectionSchema)) { return ""; } const fieldsHTML = Object.entries(sectionSchema) .map(([key, field]) => this._createFieldInput(key, field)) .join(""); return `

[${sectionName}]

${fieldsHTML}
`; } /** * Create an array section (Address, Route) * @private * @param {string} sectionName - Section name * @param {Object} arraySchema - Array section schema * @returns {string} */ _createArraySection(sectionName, arraySchema) { if (!arraySchema.items || arraySchema.items.length === 0) { return `

[${sectionName}]

No ${sectionName.toLowerCase()} sections

`; } const sectionsHTML = arraySchema.items .map((itemSchema, index) => { const fieldsHTML = Object.entries(itemSchema) .map(([key, field]) => this._createFieldInput(key, field, sectionName, index), ) .join(""); return `

[${sectionName}] ${index > 0 ? `#${index + 1}` : ""}

${fieldsHTML}
`; }) .join(""); return `
${sectionsHTML}
`; } /** * Create field input based on field schema * @private * @param {string} key - Field key * @param {Object} field - Field schema * @param {string} sectionName - Section name (for array sections) * @param {number} index - Item index (for array sections) * @returns {string} */ _createFieldInput(key, field, sectionName = null, index = null) { const dataAttributes = []; if (sectionName) { dataAttributes.push(`data-section="${sectionName}"`); dataAttributes.push(`data-index="${index}"`); } dataAttributes.push(`data-key="${key}"`); const inputId = sectionName ? `${sectionName}-${index}-${key}` : key; if (field.options?.enum) { // Create select for enum fields const optionsHTML = field.options.enum .map( (opt) => ``, ) .join(""); return `
`; } else { // Create text input for other fields return `
`; } } /** * Check if section has any values * @private * @param {Object} sectionSchema - Section schema * @returns {boolean} */ _hasSectionValues(sectionSchema) { return Object.values(sectionSchema).some( (field) => field.value !== null && field.value !== undefined && field.value !== "", ); } /** * Attach event listeners to the editor * @private */ _attachEventListeners() { // Input changes this.container.querySelectorAll(".config-input").forEach((input) => { input.addEventListener("input", (e) => this._onInputChange(e)); }); // Select changes this.container.querySelectorAll(".config-select").forEach((select) => { select.addEventListener("change", (e) => this._onSelectChange(e)); }); // Add section buttons this.container.querySelectorAll("[data-add-section]").forEach((btn) => { btn.addEventListener("click", (e) => this._onAddSection(e)); }); // Remove section buttons this.container.querySelectorAll(".remove-section").forEach((btn) => { btn.addEventListener("click", (e) => this._onRemoveSection(e)); }); } /** * Handle input changes * @private * @param {Event} event */ _onInputChange(event) { const input = event.target; this._updateFieldValue(input); } /** * Handle select changes * @private * @param {Event} event */ _onSelectChange(event) { const select = event.target; this._updateFieldValue(select); } /** * Update field value in schema * @private * @param {HTMLElement} element - Input or select element */ _updateFieldValue(element) { const section = element.dataset.section; const index = element.dataset.index ? parseInt(element.dataset.index, 10) : null; const key = element.dataset.key; const value = element.value; if (!section) { // Regular section (Match, Link, Network, DHCP) if (this.schema[section]?.[key]) { this.schema[section][key].value = value || null; } } else if ( index !== null && this.schema[section] && this.schema[section].items ) { // Array section item if (this.schema[section].items[index]?.[key]) { this.schema[section].items[index][key].value = value || null; } } } /** * Handle add section * @private * @param {Event} event */ _onAddSection(event) { const sectionName = event.target.dataset.addSection; this.emit("addSection", { section: sectionName }); } /** * Handle remove section * @private * @param {Event} event */ _onRemoveSection(event) { const section = event.target.dataset.section; const index = parseInt(event.target.dataset.index, 10); this.emit("removeSection", { section, index }); } /** * Show error message * @param {string} message - Error message */ showError(message) { if (this.container) { this.container.innerHTML = `
${message}
`; } } /** * Emit custom event * @param {string} eventName - Event name * @param {Object} detail - Event detail */ emit(eventName, detail) { const event = new CustomEvent(eventName, { detail }); this.container.dispatchEvent(event); } /** * Add event listener * @param {string} eventName - Event name * @param {Function} callback - Event callback */ on(eventName, callback) { this.container.addEventListener(eventName, callback); } /** * Get current schema * @returns {Object|null} */ getSchema() { return this.schema; } } export { StructuredEditor };