/* 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 '';
}
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 };