/* jshint esversion: 2024, module: true */ /** * Structured Editor for systemd-networkd configuration * @class StructuredEditor */ class StructuredEditor { constructor(container) { this.container = container; this.config = null; this.currentFile = ""; this.systemdNetworkModule = null; } /** * 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"); } 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()} ${this._createLinkSection()} ${this._createNetworkSection()} ${this._createDHCPSection()} ${this._createAddressSections()} ${this._createRouteSections()}
`; } _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")}
`; } _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")}
`; } _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")}
`; } _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")}
`; } _createAddressSections() { if (!this.config.Address || this.config.Address.length === 0) { return '

[Address]

No address sections

'; } 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")}
`, ).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")}
`, ).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 * @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 };