diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-09-28 14:23:58 +0300 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-09-28 14:23:58 +0300 |
| commit | 48a39bcff2c14da3d2b0c28d64ac2716d52829b0 (patch) | |
| tree | 1f10f5cf32fe08d8d8224fe80b286291c65ab54e /static/structured-editor.js | |
| parent | 645dd2b87927b7efadedb6dd5232d36a7800ce28 (diff) | |
| download | network-48a39bcff2c14da3d2b0c28d64ac2716d52829b0.tar.zst | |
Structured editor works 2
Diffstat (limited to 'static/structured-editor.js')
| -rw-r--r-- | static/structured-editor.js | 462 |
1 files changed, 225 insertions, 237 deletions
diff --git a/static/structured-editor.js b/static/structured-editor.js index 4de9217..0ce0965 100644 --- a/static/structured-editor.js +++ b/static/structured-editor.js @@ -7,44 +7,57 @@ class StructuredEditor { constructor(container) { this.container = container; - this.config = null; + this.schema = null; this.currentFile = ""; - this.systemdNetworkModule = null; } /** - * Load configuration from text - * @param {string} configText - Configuration text + * Load configuration schema + * @param {Object} schema - Configuration schema from NetworkConfiguration.getSchema() * @param {string} filename - File name */ - async loadConfiguration(configText, filename) { - // Dynamically import the systemd-network module - if (!this.systemdNetworkModule) { - this.systemdNetworkModule = await import("./systemd-network.js"); + loadSchema(schema, filename) { + if (!this.container) { + console.error("Structured editor container not found"); + return; } - const { NetworkConfiguration } = this.systemdNetworkModule; - this.config = NetworkConfiguration.fromSystemdConfiguration(configText); - this.currentFile = filename; - this.render(); + 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.config) { + if (!this.container) { + console.error("Cannot render: container is null"); + return; + } + + if (!this.schema) { this.container.innerHTML = - '<div class="error-message">No configuration loaded</div>'; + '<div class="error-message">No configuration schema loaded</div>'; return; } - this.container.innerHTML = this._createEditorHTML(); - this._attachEventListeners(); + 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 + * Create editor HTML structure from schema * @private * @returns {string} */ @@ -52,163 +65,165 @@ class StructuredEditor { return ` <div class="structured-editor"> <div class="editor-sections"> - ${this._createMatchSection()} - ${this._createLinkSection()} - ${this._createNetworkSection()} - ${this._createDHCPSection()} - ${this._createAddressSections()} - ${this._createRouteSections()} - </div> - <div class="editor-actions"> - <button class="button secondary" id="addAddressSection">Add Address</button> - <button class="button secondary" id="addRouteSection">Add Route</button> + ${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)} </div> </div> `; } - _createMatchSection() { - const match = this.config.Match; + /** + * 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 ` <div class="config-section"> - <h4>[Match]</h4> + <h4>[${sectionName}]</h4> <div class="config-table"> - ${this._createInputRow("MACAddress", match.MACAddress?.join(" ") || "", "Space-separated MAC addresses")} - ${this._createInputRow("Name", match.Name?.join(" ") || "", "Interface names")} - ${this._createInputRow("Driver", match.Driver?.join(" ") || "", "Driver names")} - ${this._createInputRow("Type", match.Type?.join(" ") || "", "Interface types")} + ${fieldsHTML} </div> </div> `; } - _createLinkSection() { - const link = this.config.Link; - return ` - <div class="config-section"> - <h4>[Link]</h4> - <div class="config-table"> - ${this._createInputRow("MACAddress", link.MACAddress || "", "Hardware address")} - ${this._createInputRow("MTUBytes", link.MTUBytes || "", "Maximum transmission unit")} - ${this._createSelectRow("WakeOnLan", link.WakeOnLan || "", ["", "phy", "unicast", "broadcast", "arp", "magic"], "Wake-on-LAN")} + /** + * 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 ` + <div class="config-section"> + <h4>[${sectionName}]</h4> + <p class="no-items">No ${sectionName.toLowerCase()} sections</p> + <button class="button small" data-add-section="${sectionName}"> + Add ${sectionName} Section + </button> </div> - </div> - `; - } + `; + } - _createNetworkSection() { - const network = this.config.Network; - return ` - <div class="config-section"> - <h4>[Network]</h4> - <div class="config-table"> - ${this._createInputRow("Description", network.Description || "", "Interface description")} - ${this._createSelectRow("DHCP", network.DHCP?.join(" ") || "", ["", "yes", "no", "ipv4", "ipv6"], "DHCP client")} - ${this._createInputRow("DNS", network.DNS?.join(" ") || "", "DNS servers")} - ${this._createInputRow("NTP", network.NTP?.join(" ") || "", "NTP servers")} - ${this._createSelectRow("IPv6PrivacyExtensions", network.IPv6PrivacyExtensions || "", ["", "yes", "no", "prefer-public"], "IPv6 privacy extensions")} + const sectionsHTML = arraySchema.items + .map((itemSchema, index) => { + const fieldsHTML = Object.entries(itemSchema) + .map(([key, field]) => + this._createFieldInput(key, field, sectionName, index), + ) + .join(""); + + return ` + <div class="config-section"> + <h4>[${sectionName}] ${index > 0 ? `#${index + 1}` : ""}</h4> + <div class="config-table"> + ${fieldsHTML} + <button class="button small warning remove-section" + data-section="${sectionName}" data-index="${index}"> + Remove + </button> + </div> </div> - </div> - `; - } + `; + }) + .join(""); - _createDHCPSection() { - const dhcp = this.config.DHCP; return ` - <div class="config-section"> - <h4>[DHCP]</h4> - <div class="config-table"> - ${this._createSelectRow("UseDNS", dhcp.UseDNS || "", ["", "yes", "no"], "Use DNS from DHCP")} - ${this._createSelectRow("UseNTP", dhcp.UseNTP || "", ["", "yes", "no"], "Use NTP from DHCP")} - ${this._createInputRow("RouteMetric", dhcp.RouteMetric || "", "Route metric")} + <div class="array-section"> + ${sectionsHTML} + <div class="config-section"> + <button class="button small" data-add-section="${sectionName}"> + Add Another ${sectionName} Section + </button> </div> </div> `; } - _createAddressSections() { - if (!this.config.Address || this.config.Address.length === 0) { - return '<div class="config-section"><h4>[Address]</h4><p class="no-items">No address sections</p></div>'; + /** + * 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}"`); } - - return this.config.Address.map( - (addr, index) => ` - <div class="config-section"> - <h4>[Address] ${index > 0 ? `#${index + 1}` : ""}</h4> - <div class="config-table"> - ${this._createInputRow("Address", addr.Address || "", "IP address with prefix")} - ${this._createInputRow("Peer", addr.Peer || "", "Peer address")} - <button class="button small warning remove-section" data-type="address" data-index="${index}">Remove</button> + 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) => + `<option value="${opt}" ${opt === field.value ? "selected" : ""}>${opt || "(not set)"}</option>`, + ) + .join(""); + + return ` + <div class="config-row"> + <label class="config-label" for="${inputId}" title="${field.description}"> + <abbr title="${field.description}">${key}</abbr>: + </label> + <select id="${inputId}" class="config-select" ${dataAttributes.join(" ")}> + ${optionsHTML} + </select> </div> - </div> - `, - ).join(""); - } - - _createRouteSections() { - if (!this.config.Route || this.config.Route.length === 0) { - return '<div class="config-section"><h4>[Route]</h4><p class="no-items">No route sections</p></div>'; - } - - return this.config.Route.map( - (route, index) => ` - <div class="config-section"> - <h4>[Route] ${index > 0 ? `#${index + 1}` : ""}</h4> - <div class="config-table"> - ${this._createInputRow("Gateway", route.Gateway || "", "Gateway address")} - ${this._createInputRow("Destination", route.Destination || "", "Destination prefix")} - ${this._createInputRow("Metric", route.Metric || "", "Route metric")} - <button class="button small warning remove-section" data-type="route" data-index="${index}">Remove</button> + `; + } else { + // Create text input for other fields + return ` + <div class="config-row"> + <label class="config-label" for="${inputId}" title="${field.description}"> + <abbr title="${field.description}">${key}</abbr>: + </label> + <input type="text" + id="${inputId}" + class="config-input" + ${dataAttributes.join(" ")} + value="${field.value || ""}" + placeholder="${field.description}"> </div> - </div> - `, - ).join(""); - } - - _createInputRow(key, value, description) { - return ` - <div class="config-row"> - <label class="config-label" title="${description}"> - <abbr title="${description}">${key}</abbr>: - </label> - <input type="text" - class="config-input" - data-section="${this._getCurrentSection()}" - data-key="${key}" - value="${value}" - placeholder="${description}"> - </div> - `; - } - - _createSelectRow(key, value, options, description) { - const optionsHTML = options - .map( - (opt) => - `<option value="${opt}" ${opt === value ? "selected" : ""}>${opt || "(not set)"}</option>`, - ) - .join(""); - - return ` - <div class="config-row"> - <label class="config-label" title="${description}"> - <abbr title="${description}">${key}</abbr>: - </label> - <select class="config-select" data-section="${this._getCurrentSection()}" data-key="${key}"> - ${optionsHTML} - </select> - </div> - `; + `; + } } /** - * Get current section name for event handling + * Check if section has any values * @private - * @returns {string} + * @param {Object} sectionSchema - Section schema + * @returns {boolean} */ - _getCurrentSection() { - // This is a simplified implementation - you might want to track the current section more precisely - return "network"; + _hasSectionValues(sectionSchema) { + return Object.values(sectionSchema).some( + (field) => + field.value !== null && field.value !== undefined && field.value !== "", + ); } /** @@ -226,20 +241,12 @@ class StructuredEditor { select.addEventListener("change", (e) => this._onSelectChange(e)); }); - // Add sections - this.container - .querySelector("#addAddressSection") - ?.addEventListener("click", () => { - this._addAddressSection(); - }); - - this.container - .querySelector("#addRouteSection") - ?.addEventListener("click", () => { - this._addRouteSection(); - }); + // Add section buttons + this.container.querySelectorAll("[data-add-section]").forEach((btn) => { + btn.addEventListener("click", (e) => this._onAddSection(e)); + }); - // Remove sections + // Remove section buttons this.container.querySelectorAll(".remove-section").forEach((btn) => { btn.addEventListener("click", (e) => this._onRemoveSection(e)); }); @@ -252,11 +259,7 @@ class StructuredEditor { */ _onInputChange(event) { const input = event.target; - const section = input.dataset.section; - const key = input.dataset.key; - const value = input.value; - - this._updateConfigValue(section, key, value); + this._updateFieldValue(input); } /** @@ -266,114 +269,99 @@ class StructuredEditor { */ _onSelectChange(event) { const select = event.target; - const section = select.dataset.section; - const key = select.dataset.key; - const value = select.value; - - this._updateConfigValue(section, key, value); + this._updateFieldValue(select); } /** - * Update configuration value + * Update field value in schema * @private - * @param {string} section - Section name - * @param {string} key - Key name - * @param {string} value - Value + * @param {HTMLElement} element - Input or select element */ - _updateConfigValue(section, key, value) { - if (!this.config) return; - - // Simplified implementation - you'll want to expand this based on your systemd-network.js structure - console.log(`Update ${section}.${key} = ${value}`); - - // Example update logic - you'll need to implement this based on your actual data structure - switch (section) { - case "match": - if (["MACAddress", "Name", "Driver", "Type"].includes(key)) { - this.config.Match[key] = value.split(" ").filter((v) => v.trim()); - } else { - this.config.Match[key] = value; - } - break; - case "link": - this.config.Link[key] = value; - break; - case "network": - if (["DNS", "NTP", "DHCP", "Domains", "IPForward"].includes(key)) { - this.config.Network[key] = value.split(" ").filter((v) => v.trim()); - } else { - this.config.Network[key] = value; - } - break; - case "dhcp": - this.config.DHCP[key] = value; - break; + _updateFieldValue(element) { + const section = element.dataset.section; + const index = element.dataset.index + ? parseInt(element.dataset.index) + : null; + const key = element.dataset.key; + const value = element.value; + + if (!section) { + // Regular section (Match, Link, Network, DHCP) + if (this.schema[section] && 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] && + this.schema[section].items[index][key] + ) { + this.schema[section].items[index][key].value = value || null; + } } } /** - * Add a new address section + * Handle add section * @private + * @param {Event} event */ - async _addAddressSection() { - if (!this.systemdNetworkModule) { - this.systemdNetworkModule = await import("./systemd-network.js"); - } - - const { AddressSection } = this.systemdNetworkModule; - this.config.Address.push(new AddressSection()); - this.render(); + _onAddSection(event) { + const sectionName = event.target.dataset.addSection; + this.emit("addSection", { section: sectionName }); } /** - * Add a new route section + * Handle remove section * @private + * @param {Event} event */ - async _addRouteSection() { - if (!this.systemdNetworkModule) { - this.systemdNetworkModule = await import("./systemd-network.js"); - } - - const { RouteSection } = this.systemdNetworkModule; - this.config.Route.push(new RouteSection()); - this.render(); + _onRemoveSection(event) { + const section = event.target.dataset.section; + const index = parseInt(event.target.dataset.index); + this.emit("removeSection", { section, index }); } /** - * Remove a section - * @private - * @param {Event} event + * Show error message + * @param {string} message - Error message */ - _onRemoveSection(event) { - const btn = event.target; - const type = btn.dataset.type; - const index = parseInt(btn.dataset.index); - - if (type === "address" && this.config.Address) { - this.config.Address.splice(index, 1); - } else if (type === "route" && this.config.Route) { - this.config.Route.splice(index, 1); + showError(message) { + if (this.container) { + this.container.innerHTML = `<div class="error-message">${message}</div>`; } + } - this.render(); + /** + * 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); } /** - * Get current configuration as text - * @returns {string} + * Add event listener + * @param {string} eventName - Event name + * @param {Function} callback - Event callback */ - getConfigurationText() { - return this.config ? this.config.toSystemdConfiguration() : ""; + on(eventName, callback) { + this.container.addEventListener(eventName, callback); } /** - * Get current configuration object + * Get current schema * @returns {Object|null} */ - getConfiguration() { - return this.config; + getSchema() { + return this.schema; } } export { StructuredEditor }; - |
