From 48a39bcff2c14da3d2b0c28d64ac2716d52829b0 Mon Sep 17 00:00:00 2001 From: Petri Hienonen Date: Sun, 28 Sep 2025 14:23:58 +0300 Subject: Structured editor works 2 --- static/structured-editor.js | 462 +++++++++++++++++++++----------------------- 1 file changed, 225 insertions(+), 237 deletions(-) (limited to 'static/structured-editor.js') 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 = - '
No configuration loaded
'; + '
No configuration schema loaded
'; 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 `
- ${this._createMatchSection()} - ${this._createLinkSection()} - ${this._createNetworkSection()} - ${this._createDHCPSection()} - ${this._createAddressSections()} - ${this._createRouteSections()} -
-
- - + ${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)}
`; } - _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 `
-

[Match]

+

[${sectionName}]

- ${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}
`; } - _createLinkSection() { - const link = this.config.Link; - return ` -
-

[Link]

-
- ${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 ` +
+

[${sectionName}]

+

No ${sectionName.toLowerCase()} sections

+
-
- `; - } + `; + } - _createNetworkSection() { - const network = this.config.Network; - return ` -
-

[Network]

-
- ${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 ` +
+

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

+
+ ${fieldsHTML} + +
-
- `; - } + `; + }) + .join(""); - _createDHCPSection() { - const dhcp = this.config.DHCP; return ` -
-

[DHCP]

-
- ${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")} +
+ ${sectionsHTML} +
+
`; } - _createAddressSections() { - if (!this.config.Address || this.config.Address.length === 0) { - return '

[Address]

No address sections

'; + /** + * 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) => ` -
-

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

-
- ${this._createInputRow("Address", addr.Address || "", "IP address with prefix")} - ${this._createInputRow("Peer", addr.Peer || "", "Peer address")} - + 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 ` +
+ +
-
- `, - ).join(""); - } - - _createRouteSections() { - if (!this.config.Route || this.config.Route.length === 0) { - return '

[Route]

No route sections

'; - } - - return this.config.Route.map( - (route, index) => ` -
-

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

-
- ${this._createInputRow("Gateway", route.Gateway || "", "Gateway address")} - ${this._createInputRow("Destination", route.Destination || "", "Destination prefix")} - ${this._createInputRow("Metric", route.Metric || "", "Route metric")} - + `; + } else { + // Create text input for other fields + return ` +
+ +
-
- `, - ).join(""); - } - - _createInputRow(key, value, description) { - return ` -
- - -
- `; - } - - _createSelectRow(key, value, options, description) { - const optionsHTML = options - .map( - (opt) => - ``, - ) - .join(""); - - return ` -
- - -
- `; + `; + } } /** - * 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 = `
${message}
`; } + } - 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 }; - -- cgit v1.2.3-70-g09d2