From 645dd2b87927b7efadedb6dd5232d36a7800ce28 Mon Sep 17 00:00:00 2001 From: Petri Hienonen Date: Sun, 28 Sep 2025 14:15:46 +0300 Subject: Structured editor works --- static/structured-editor.js | 468 ++++++++++++++++++++++++++++---------------- 1 file changed, 297 insertions(+), 171 deletions(-) (limited to 'static/structured-editor.js') diff --git a/static/structured-editor.js b/static/structured-editor.js index dc46bef..4de9217 100644 --- a/static/structured-editor.js +++ b/static/structured-editor.js @@ -2,44 +2,54 @@ /** * Structured Editor for systemd-networkd configuration - * @module StructuredEditor + * @class StructuredEditor */ +class StructuredEditor { + constructor(container) { + this.container = container; + this.config = null; + this.currentFile = ""; + this.systemdNetworkModule = null; + } -import { NetworkConfiguration } from './systemd-network.js'; + /** + * Load configuration from text + * @param {string} configText - Configuration text + * @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"); + } -class StructuredEditor { - constructor(container) { - this.container = container; - this.config = new NetworkConfiguration(); - this.currentFile = ''; - } - - /** - * Load configuration from text - * @param {string} configText - Configuration text - * @param {string} filename - File name - */ - loadConfiguration(configText, filename) { - this.config = NetworkConfiguration.fromSystemdConfiguration(configText); - this.currentFile = filename; - this.render(); - } - - /** - * Render the structured editor - */ - render() { - this.container.innerHTML = this._createEditorHTML(); - this._attachEventListeners(); - } - - /** - * Create editor HTML structure - * @private - * @returns {string} - */ - _createEditorHTML() { - return ` + const { NetworkConfiguration } = this.systemdNetworkModule; + this.config = NetworkConfiguration.fromSystemdConfiguration(configText); + this.currentFile = filename; + this.render(); + } + + /** + * Render the structured editor + */ + render() { + if (!this.config) { + this.container.innerHTML = + '
No configuration loaded
'; + return; + } + + this.container.innerHTML = this._createEditorHTML(); + this._attachEventListeners(); + } + + /** + * Create editor HTML structure + * @private + * @returns {string} + */ + _createEditorHTML() { + return `
${this._createMatchSection()} @@ -50,204 +60,320 @@ class StructuredEditor { ${this._createRouteSections()}
- - - + +
`; - } + } - _createMatchSection() { - const match = this.config.Match; - return ` + _createMatchSection() { + const match = this.config.Match; + return `

[Match]

- ${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')} + ${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")}
`; - } + } - _createLinkSection() { - const link = this.config.Link; - return ` + _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')} + ${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")}
`; - } + } - _createNetworkSection() { - const network = this.config.Network; - return ` + _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')} + ${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")}
`; - } + } - _createDHCPSection() { - const dhcp = this.config.DHCP; - return ` + _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')} + ${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")}
`; - } + } + + _createAddressSections() { + if (!this.config.Address || this.config.Address.length === 0) { + return '

[Address]

No address sections

'; + } - _createAddressSections() { - return this.config.Address.map((addr, index) => ` + return this.config.Address.map( + (addr, index) => `
-

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

+

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

- ${this._createInputRow('Address', addr.Address, 'IP address with prefix')} - ${this._createInputRow('Peer', addr.Peer, 'Peer address')} - + ${this._createInputRow("Address", addr.Address || "", "IP address with prefix")} + ${this._createInputRow("Peer", addr.Peer || "", "Peer address")} +
- `).join(''); - } + `, + ).join(""); + } - _createRouteSections() { - return this.config.Route.map((route, index) => ` + _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}` : ''}

+

[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')} - + ${this._createInputRow("Gateway", route.Gateway || "", "Gateway address")} + ${this._createInputRow("Destination", route.Destination || "", "Destination prefix")} + ${this._createInputRow("Metric", route.Metric || "", "Route metric")} +
- `).join(''); - } + `, + ).join(""); + } - _createInputRow(key, value, description) { - return ` + _createInputRow(key, value, description) { + return `
`; - } + } - _createSelectRow(key, value, options, description) { - const optionsHTML = options.map(opt => - `` - ).join(''); + _createSelectRow(key, value, options, description) { + const optionsHTML = options + .map( + (opt) => + ``, + ) + .join(""); - return ` + return `
- ${optionsHTML}
`; - } - - _attachEventListeners() { - // Input changes - this.container.querySelectorAll('.config-input').forEach(input => { - input.addEventListener('change', (e) => this._onInputChange(e)); - }); - - // Select changes - this.container.querySelectorAll('.config-select').forEach(select => { - select.addEventListener('change', (e) => this._onSelectChange(e)); - }); - - // Add sections - this.container.querySelector('#addAddressSection')?.addEventListener('click', () => { - this.config.Address.push(new (await import('./systemd-network.js')).AddressSection()); - this.render(); - }); - - this.container.querySelector('#addRouteSection')?.addEventListener('click', () => { - this.config.Route.push(new (await import('./systemd-network.js')).RouteSection()); - this.render(); - }); - - // Remove sections - this.container.querySelectorAll('.remove-section').forEach(btn => { - btn.addEventListener('click', (e) => this._onRemoveSection(e)); - }); - } - - _onInputChange(event) { - const input = event.target; - const key = input.dataset.key; - const value = input.value; - - // Update configuration based on context - this._updateConfigValue(key, value); - } - - _onSelectChange(event) { - const select = event.target; - const key = select.dataset.key; - const value = select.value; - - this._updateConfigValue(key, value); - } - - _updateConfigValue(key, value) { - // This would need to be implemented based on the current section context - console.log(`Update ${key} = ${value}`); - // Implementation would update the this.config object - } - - _onRemoveSection(event) { - const btn = event.target; - const type = btn.dataset.type; - const index = parseInt(btn.dataset.index); - - if (type === 'address') { - this.config.Address.splice(index, 1); - } else if (type === 'route') { - this.config.Route.splice(index, 1); - } - - this.render(); - } - - /** - * Get current configuration as text - * @returns {string} - */ - getConfigurationText() { - return this.config.toSystemdConfiguration(); - } + } + + /** + * Get current section name for event handling + * @private + * @returns {string} + */ + _getCurrentSection() { + // This is a simplified implementation - you might want to track the current section more precisely + return "network"; + } + + /** + * 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 sections + this.container + .querySelector("#addAddressSection") + ?.addEventListener("click", () => { + this._addAddressSection(); + }); + + this.container + .querySelector("#addRouteSection") + ?.addEventListener("click", () => { + this._addRouteSection(); + }); + + // Remove sections + 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; + const section = input.dataset.section; + const key = input.dataset.key; + const value = input.value; + + this._updateConfigValue(section, key, value); + } + + /** + * Handle select changes + * @private + * @param {Event} event + */ + _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); + } + + /** + * Update configuration value + * @private + * @param {string} section - Section name + * @param {string} key - Key name + * @param {string} value - Value + */ + _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; + } + } + + /** + * Add a new address section + * @private + */ + async _addAddressSection() { + if (!this.systemdNetworkModule) { + this.systemdNetworkModule = await import("./systemd-network.js"); + } + + const { AddressSection } = this.systemdNetworkModule; + this.config.Address.push(new AddressSection()); + this.render(); + } + + /** + * Add a new route section + * @private + */ + async _addRouteSection() { + if (!this.systemdNetworkModule) { + this.systemdNetworkModule = await import("./systemd-network.js"); + } + + const { RouteSection } = this.systemdNetworkModule; + this.config.Route.push(new RouteSection()); + this.render(); + } + + /** + * Remove a section + * @private + * @param {Event} event + */ + _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); + } + + this.render(); + } + + /** + * Get current configuration as text + * @returns {string} + */ + getConfigurationText() { + return this.config ? this.config.toSystemdConfiguration() : ""; + } + + /** + * Get current configuration object + * @returns {Object|null} + */ + getConfiguration() { + return this.config; + } } export { StructuredEditor }; + -- cgit v1.2.3-70-g09d2