summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--static/app.js30
-rw-r--r--static/structured-editor.js462
-rw-r--r--static/systemd-network.js590
3 files changed, 529 insertions, 553 deletions
diff --git a/static/app.js b/static/app.js
index ca205f3..3599cba 100644
--- a/static/app.js
+++ b/static/app.js
@@ -1,10 +1,10 @@
/* jshint esversion: 2024, module: true */
-import { ThemeManager } from './theme-manager.js';
import { ApiClient } from './api-client.js';
-import { InterfaceRenderer } from './interface-renderer.js';
import { ConfigManager } from './config-manager.js';
+import { InterfaceRenderer } from './interface-renderer.js';
import { StructuredEditor } from './structured-editor.js';
+import { ThemeManager } from './theme-manager.js';
/**
* Main Application Class
@@ -160,6 +160,17 @@ class Application {
document.addEventListener('touchstart', this.handleTouchStart, { passive: true });
}
+ // Add event handlers for structured editor
+ handleAddSection(detail) {
+ console.log('Add section:', detail);
+ // Implement section addition logic
+ }
+
+ handleRemoveSection(detail) {
+ console.log('Remove section:', detail);
+ // Implement section removal logic
+ }
+
/**
* Handle configuration file change
* @method handleConfigFileChange
@@ -181,6 +192,7 @@ class Application {
* Load config for structured editor
* @method loadConfigForStructuredEditor
*/
+
async loadConfigForStructuredEditor() {
if (!this.structuredEditor) {
console.error('Structured editor not initialized');
@@ -189,7 +201,19 @@ class Application {
try {
const text = await this.apiClient.getText(`/api/config/${encodeURIComponent(this.state.currentConfigFile)}`);
- await this.structuredEditor.loadConfiguration(text, this.state.currentConfigFile);
+
+ // Parse configuration and get schema
+ const { NetworkConfiguration } = await import('./systemd-network.js');
+ const config = NetworkConfiguration.fromSystemdConfiguration(text);
+ const schema = config.getSchema();
+
+ // Load schema into structured editor
+ this.structuredEditor.loadSchema(schema, this.state.currentConfigFile);
+
+ // Set up event listeners for structured editor
+ this.structuredEditor.on('addSection', (event) => this.handleAddSection(event.detail));
+ this.structuredEditor.on('removeSection', (event) => this.handleRemoveSection(event.detail));
+
} catch (error) {
alert(`Failed to load config for structured editor: ${error.message}`);
}
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 };
-
diff --git a/static/systemd-network.js b/static/systemd-network.js
index 8c66a6d..1a33f0e 100644
--- a/static/systemd-network.js
+++ b/static/systemd-network.js
@@ -7,56 +7,71 @@
*/
/**
- * MAC Address type
- * @class MACAddress
+ * Base field type with standardized interface
+ * @class BaseField
*/
-class MACAddress {
+class BaseField {
/**
- * @param {string} value - MAC address value
- * @param {string} description - Description
+ * @param {*} value - Field value
+ * @param {string} type - Field type
+ * @param {string} description - Field description
+ * @param {Object} options - Additional options (pattern, enum, etc.)
*/
- constructor(value = '', description = 'Hardware address') {
+ constructor(value = null, type = 'string', description = '', options = {}) {
this.value = value;
- this.type = 'mac-address';
+ this.type = type;
this.description = description;
- this.pattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
+ this.options = options;
}
/**
- * Validate MAC address format
+ * Validate field value
* @returns {boolean}
*/
validate() {
- return this.pattern.test(this.value);
+ if (this.value === null || this.value === undefined) return true;
+
+ if (this.options.pattern && typeof this.value === 'string') {
+ return this.options.pattern.test(this.value);
+ }
+
+ if (this.options.enum && this.options.enum.length > 0) {
+ return this.options.enum.includes(this.value);
+ }
+
+ return true;
}
+ /**
+ * Convert to string representation
+ * @returns {string}
+ */
toString() {
- return this.value;
+ return this.value !== null ? String(this.value) : '';
}
}
/**
- * IPv4 Address type
- * @class IPv4Address
+ * MAC Address type
+ * @class MACAddress
*/
-class IPv4Address {
- /**
- * @param {string} value - IPv4 address
- * @param {string} description - Description
- */
- constructor(value = '', description = 'IPv4 address') {
- this.value = value;
- this.type = 'ipv4-address';
- this.description = description;
- this.pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
- }
-
- validate() {
- return this.pattern.test(this.value);
+class MACAddress extends BaseField {
+ constructor(value = null) {
+ super(value, 'mac-address', 'Hardware address (MAC)', {
+ pattern: /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/
+ });
}
+}
- toString() {
- return this.value;
+/**
+ * IPv4 Address type
+ * @class IPv4Address
+ */
+class IPv4Address extends BaseField {
+ constructor(value = null) {
+ super(value, 'ipv4-address', 'IPv4 address', {
+ pattern: /^(\d{1,3}\.){3}\d{1,3}$/
+ });
}
}
@@ -64,24 +79,47 @@ class IPv4Address {
* IPv6 Address type
* @class IPv6Address
*/
-class IPv6Address {
- /**
- * @param {string} value - IPv6 address
- * @param {string} description - Description
- */
- constructor(value = '', description = 'IPv6 address') {
- this.value = value;
- this.type = 'ipv6-address';
- this.description = description;
- this.pattern = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
+class IPv6Address extends BaseField {
+ constructor(value = null) {
+ super(value, 'ipv6-address', 'IPv6 address', {
+ pattern: /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/
+ });
}
+}
- validate() {
- return this.pattern.test(this.value);
+/**
+ * Boolean type with yes/no values
+ * @class BooleanYesNo
+ */
+class BooleanYesNo extends BaseField {
+ constructor(value = null) {
+ super(value, 'boolean', 'Boolean (yes/no)', {
+ enum: ['yes', 'no']
+ });
}
+}
- toString() {
- return this.value;
+/**
+ * Port type
+ * @class Port
+ */
+class Port extends BaseField {
+ constructor(value = null) {
+ super(value, 'port', 'Network port (1-65535)', {
+ pattern: /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/
+ });
+ }
+}
+
+/**
+ * MTU type
+ * @class MTU
+ */
+class MTU extends BaseField {
+ constructor(value = null) {
+ super(value, 'mtu', 'Maximum Transmission Unit', {
+ pattern: /^[1-9][0-9]*$/
+ });
}
}
@@ -91,28 +129,17 @@ class IPv6Address {
*/
class MatchSection {
constructor() {
- /** @type {MACAddress[]} */
- this.MACAddress = [];
- /** @type {string[]} */
- this.OriginalName = [];
- /** @type {string[]} */
- this.Path = [];
- /** @type {string[]} */
- this.Driver = [];
- /** @type {string[]} */
- this.Type = [];
- /** @type {string[]} */
- this.Name = [];
- /** @type {string} */
- this.Property = '';
- /** @type {string} */
- this.Host = '';
- /** @type {string} */
- this.Virtualization = '';
- /** @type {string} */
- this.KernelCommandLine = '';
- /** @type {string} */
- this.Architecture = '';
+ this.MACAddress = new BaseField(null, 'mac-addresses', 'Space-separated MAC addresses');
+ this.OriginalName = new BaseField(null, 'strings', 'Original interface names');
+ this.Path = new BaseField(null, 'strings', 'Device path patterns');
+ this.Driver = new BaseField(null, 'strings', 'Driver names');
+ this.Type = new BaseField(null, 'strings', 'Interface types (ether, wifi, etc.)');
+ this.Name = new BaseField(null, 'strings', 'Interface names');
+ this.Property = new BaseField(null, 'string', 'Device property');
+ this.Host = new BaseField(null, 'string', 'Host name');
+ this.Virtualization = new BaseField(null, 'string', 'Virtualization detection');
+ this.KernelCommandLine = new BaseField(null, 'string', 'Kernel command line');
+ this.Architecture = new BaseField(null, 'string', 'System architecture');
}
}
@@ -122,26 +149,22 @@ class MatchSection {
*/
class LinkSection {
constructor() {
- /** @type {string} */
- this.MACAddress = '';
- /** @type {string} */
- this.MTUBytes = '';
- /** @type {number} */
- this.BitsPerSecond = 0;
- /** @type {string} */
- this.Duplex = '';
- /** @type {string} */
- this.AutoNegotiation = '';
- /** @type {string} */
- this.WakeOnLan = '';
- /** @type {string} */
- this.Port = '';
- /** @type {string} */
- this.Advertise = '';
- /** @type {string} */
- this.RxFlowControl = '';
- /** @type {string} */
- this.TxFlowControl = '';
+ this.MACAddress = new MACAddress();
+ this.MTUBytes = new MTU();
+ this.BitsPerSecond = new BaseField(null, 'number', 'Link speed in bits per second');
+ this.Duplex = new BaseField(null, 'string', 'Duplex mode', {
+ enum: ['half', 'full']
+ });
+ this.AutoNegotiation = new BooleanYesNo();
+ this.WakeOnLan = new BaseField(null, 'string', 'Wake-on-LAN', {
+ enum: ['phy', 'unicast', 'broadcast', 'arp', 'magic', '']
+ });
+ this.Port = new BaseField(null, 'string', 'Port type', {
+ enum: ['tp', 'aui', 'bnc', 'mii', 'fibre', '']
+ });
+ this.Advertise = new BaseField(null, 'strings', 'Advertised features');
+ this.RxFlowControl = new BooleanYesNo();
+ this.TxFlowControl = new BooleanYesNo();
}
}
@@ -151,36 +174,33 @@ class LinkSection {
*/
class NetworkSection {
constructor() {
- /** @type {string} */
- this.Description = '';
- /** @type {string[]} */
- this.DHCP = []; // 'yes', 'no', 'ipv4', 'ipv6'
- /** @type {boolean} */
- this.DHCPServer = false;
- /** @type {string[]} */
- this.DNS = [];
- /** @type {string[]} */
- this.NTP = [];
- /** @type {string[]} */
- this.IPForward = []; // 'yes', 'no', 'ipv4', 'ipv6'
- /** @type {string} */
- this.IPv6PrivacyExtensions = ''; // 'yes', 'no', 'prefer-public'
- /** @type {string} */
- this.IPv6AcceptRA = ''; // 'yes', 'no'
- /** @type {string} */
- this.LLMNR = ''; // 'yes', 'no', 'resolve'
- /** @type {string} */
- this.MulticastDNS = ''; // 'yes', 'no', 'resolve'
- /** @type {string} */
- this.DNSSEC = ''; // 'yes', 'no', 'allow-downgrade'
- /** @type {string[]} */
- this.Domains = [];
- /** @type {string} */
- this.ConfigureWithoutCarrier = ''; // 'yes', 'no'
- /** @type {string} */
- this.IgnoreCarrierLoss = ''; // 'yes', 'no'
- /** @type {number} */
- this.KeepConfiguration = 0; // seconds
+ this.Description = new BaseField(null, 'string', 'Interface description');
+ this.DHCP = new BaseField(null, 'dhcp-mode', 'DHCP client', {
+ enum: ['yes', 'no', 'ipv4', 'ipv6']
+ });
+ this.DHCPServer = new BooleanYesNo();
+ this.DNS = new BaseField(null, 'ip-addresses', 'DNS servers');
+ this.NTP = new BaseField(null, 'ip-addresses', 'NTP servers');
+ this.IPForward = new BaseField(null, 'ip-forward', 'IP forwarding', {
+ enum: ['yes', 'no', 'ipv4', 'ipv6']
+ });
+ this.IPv6PrivacyExtensions = new BaseField(null, 'privacy-extensions', 'IPv6 privacy extensions', {
+ enum: ['yes', 'no', 'prefer-public']
+ });
+ this.IPv6AcceptRA = new BooleanYesNo();
+ this.LLMNR = new BaseField(null, 'llmnr', 'LLMNR support', {
+ enum: ['yes', 'no', 'resolve']
+ });
+ this.MulticastDNS = new BaseField(null, 'mdns', 'Multicast DNS', {
+ enum: ['yes', 'no', 'resolve']
+ });
+ this.DNSSEC = new BaseField(null, 'dnssec', 'DNSSEC support', {
+ enum: ['yes', 'no', 'allow-downgrade']
+ });
+ this.Domains = new BaseField(null, 'strings', 'DNS search domains');
+ this.ConfigureWithoutCarrier = new BooleanYesNo();
+ this.IgnoreCarrierLoss = new BooleanYesNo();
+ this.KeepConfiguration = new BaseField(null, 'number', 'Keep configuration time in seconds');
}
}
@@ -190,20 +210,17 @@ class NetworkSection {
*/
class DHCPSection {
constructor() {
- /** @type {string} */
- this.UseDNS = ''; // 'yes', 'no'
- /** @type {string} */
- this.UseNTP = ''; // 'yes', 'no'
- /** @type {string} */
- this.UseMTU = ''; // 'yes', 'no'
- /** @type {string} */
- this.UseHostname = ''; // 'yes', 'no'
- /** @type {string} */
- this.UseDomains = ''; // 'yes', 'no', 'route'
- /** @type {string} */
- this.ClientIdentifier = ''; // 'mac', 'duid'
- /** @type {string} */
- this.RouteMetric = '';
+ this.UseDNS = new BooleanYesNo();
+ this.UseNTP = new BooleanYesNo();
+ this.UseMTU = new BooleanYesNo();
+ this.UseHostname = new BooleanYesNo();
+ this.UseDomains = new BaseField(null, 'use-domains', 'Use domains from DHCP', {
+ enum: ['yes', 'no', 'route']
+ });
+ this.ClientIdentifier = new BaseField(null, 'client-identifier', 'DHCP client identifier', {
+ enum: ['mac', 'duid']
+ });
+ this.RouteMetric = new BaseField(null, 'number', 'Route metric for DHCP routes');
}
}
@@ -213,18 +230,12 @@ class DHCPSection {
*/
class AddressSection {
constructor() {
- /** @type {string} */
- this.Address = ''; // IP address with prefix
- /** @type {string} */
- this.Peer = ''; // Peer address
- /** @type {string} */
- this.Broadcast = ''; // Broadcast address
- /** @type {string} */
- this.Label = ''; // Address label
- /** @type {number} */
- this.Scope = 0; // Address scope
- /** @type {string} */
- this.Flags = ''; // Address flags
+ this.Address = new BaseField(null, 'ip-prefix', 'IP address with prefix');
+ this.Peer = new BaseField(null, 'ip-address', 'Peer address');
+ this.Broadcast = new BaseField(null, 'ip-address', 'Broadcast address');
+ this.Label = new BaseField(null, 'string', 'Address label');
+ this.Scope = new BaseField(null, 'number', 'Address scope');
+ this.Flags = new BaseField(null, 'strings', 'Address flags');
}
}
@@ -234,22 +245,18 @@ class AddressSection {
*/
class RouteSection {
constructor() {
- /** @type {string} */
- this.Gateway = ''; // Gateway address
- /** @type {string} */
- this.GatewayOnLink = ''; // 'yes', 'no'
- /** @type {string} */
- this.Destination = ''; // Destination prefix
- /** @type {string} */
- this.Source = ''; // Source address
- /** @type {string} */
- this.PreferredSource = ''; // Preferred source address
- /** @type {number} */
- this.Metric = 1024; // Route metric
- /** @type {string} */
- this.Scope = ''; // 'global', 'link', 'host'
- /** @type {string} */
- this.Type = ''; // 'unicast', 'local', 'broadcast', etc.
+ this.Gateway = new BaseField(null, 'ip-address', 'Gateway address');
+ this.GatewayOnLink = new BooleanYesNo();
+ this.Destination = new BaseField(null, 'ip-prefix', 'Destination prefix');
+ this.Source = new BaseField(null, 'ip-address', 'Source address');
+ this.PreferredSource = new BaseField(null, 'ip-address', 'Preferred source address');
+ this.Metric = new BaseField(null, 'number', 'Route metric');
+ this.Scope = new BaseField(null, 'route-scope', 'Route scope', {
+ enum: ['global', 'link', 'host']
+ });
+ this.Type = new BaseField(null, 'route-type', 'Route type', {
+ enum: ['unicast', 'local', 'broadcast', 'anycast', 'multicast', 'blackhole', 'unreachable', 'prohibit']
+ });
}
}
@@ -259,21 +266,64 @@ class RouteSection {
*/
class NetworkConfiguration {
constructor() {
- /** @type {MatchSection} */
this.Match = new MatchSection();
- /** @type {LinkSection} */
this.Link = new LinkSection();
- /** @type {NetworkSection} */
this.Network = new NetworkSection();
- /** @type {DHCPSection} */
this.DHCP = new DHCPSection();
- /** @type {AddressSection[]} */
this.Address = [];
- /** @type {RouteSection[]} */
this.Route = [];
}
/**
+ * Get schema for structured editor
+ * @returns {Object} Schema definition
+ */
+ getSchema() {
+ return {
+ Match: this._getSectionSchema(this.Match),
+ Link: this._getSectionSchema(this.Link),
+ Network: this._getSectionSchema(this.Network),
+ DHCP: this._getSectionSchema(this.DHCP),
+ Address: this._getArraySectionSchema(AddressSection, 'Address'),
+ Route: this._getArraySectionSchema(RouteSection, 'Route')
+ };
+ }
+
+ /**
+ * Get schema for a single section
+ * @private
+ * @param {Object} section - Section instance
+ * @returns {Object} Section schema
+ */
+ _getSectionSchema(section) {
+ const schema = {};
+ for (const [key, field] of Object.entries(section)) {
+ schema[key] = {
+ value: field.value,
+ type: field.type,
+ description: field.description,
+ options: field.options
+ };
+ }
+ return schema;
+ }
+
+ /**
+ * Get schema for array sections (Address, Route)
+ * @private
+ * @param {Class} SectionClass - Section class
+ * @param {string} sectionName - Section name
+ * @returns {Object} Array section schema
+ */
+ _getArraySectionSchema(SectionClass, sectionName) {
+ const template = new SectionClass();
+ return {
+ itemSchema: this._getSectionSchema(template),
+ items: this[sectionName].map(item => this._getSectionSchema(item))
+ };
+ }
+
+ /**
* Parse systemd network configuration from text
* @param {string} configText - Configuration file content
* @returns {NetworkConfiguration}
@@ -282,6 +332,8 @@ class NetworkConfiguration {
const config = new NetworkConfiguration();
const lines = configText.split('\n');
let currentSection = null;
+ let currentAddress = null;
+ let currentRoute = null;
for (const line of lines) {
const trimmed = line.trim();
@@ -293,6 +345,15 @@ class NetworkConfiguration {
const sectionMatch = trimmed.match(/^\[(\w+)\]$/);
if (sectionMatch) {
currentSection = sectionMatch[1].toLowerCase();
+
+ // Start new array sections
+ if (currentSection === 'address') {
+ currentAddress = new AddressSection();
+ config.Address.push(currentAddress);
+ } else if (currentSection === 'route') {
+ currentRoute = new RouteSection();
+ config.Route.push(currentRoute);
+ }
continue;
}
@@ -302,7 +363,7 @@ class NetworkConfiguration {
const key = kvMatch[1];
const value = kvMatch[2];
- config._setValue(currentSection, key, value);
+ config._setValue(currentSection, key, value, currentAddress, currentRoute);
}
}
@@ -315,8 +376,10 @@ class NetworkConfiguration {
* @param {string} section - Section name
* @param {string} key - Key name
* @param {string} value - Value
+ * @param {AddressSection} currentAddress - Current address section
+ * @param {RouteSection} currentRoute - Current route section
*/
- _setValue(section, key, value) {
+ _setValue(section, key, value, currentAddress, currentRoute) {
switch (section) {
case 'match':
this._setMatchValue(key, value);
@@ -331,70 +394,52 @@ class NetworkConfiguration {
this._setDHCPValue(key, value);
break;
case 'address':
- // Handle multiple address sections
- if (!this.Address.length) this.Address.push(new AddressSection());
- this._setAddressValue(this.Address[this.Address.length - 1], key, value);
+ if (currentAddress) {
+ this._setAddressValue(currentAddress, key, value);
+ }
break;
case 'route':
- // Handle multiple route sections
- if (!this.Route.length) this.Route.push(new RouteSection());
- this._setRouteValue(this.Route[this.Route.length - 1], key, value);
+ if (currentRoute) {
+ this._setRouteValue(currentRoute, key, value);
+ }
break;
}
}
_setMatchValue(key, value) {
- const match = this.Match;
- switch (key) {
- case 'MACAddress':
- match.MACAddress = value.split(' ');
- break;
- case 'Name':
- match.Name = value.split(' ');
- break;
- case 'Driver':
- match.Driver = value.split(' ');
- break;
- case 'Path':
- match.Path = value.split(' ');
- break;
- case 'Type':
- match.Type = value.split(' ');
- break;
- default:
- match[key] = value;
+ if (this.Match[key] !== undefined) {
+ this.Match[key].value = value;
}
}
_setLinkValue(key, value) {
- this.Link[key] = value;
+ if (this.Link[key] !== undefined) {
+ this.Link[key].value = value;
+ }
}
_setNetworkValue(key, value) {
- const network = this.Network;
- switch (key) {
- case 'DNS':
- case 'NTP':
- case 'Domains':
- case 'DHCP':
- case 'IPForward':
- network[key] = value.split(' ');
- break;
- default:
- network[key] = value;
+ if (this.Network[key] !== undefined) {
+ this.Network[key].value = value;
}
}
_setDHCPValue(key, value) {
- this.DHCP[key] = value;
+ if (this.DHCP[key] !== undefined) {
+ this.DHCP[key].value = value;
+ }
}
_setAddressValue(address, key, value) {
- address[key] = value;
+ if (address[key] !== undefined) {
+ address[key].value = value;
+ }
}
_setRouteValue(route, key, value) {
- route[key] = value;
+ if (route[key] !== undefined) {
+ route[key].value = value;
+ }
}
/**
@@ -405,155 +450,74 @@ class NetworkConfiguration {
const sections = [];
// [Match] section
- if (this._hasMatchValues()) {
+ if (this._hasSectionValues(this.Match)) {
sections.push('[Match]');
- sections.push(...this._formatMatchSection());
+ sections.push(...this._formatSection(this.Match));
}
// [Link] section
- if (this._hasLinkValues()) {
+ if (this._hasSectionValues(this.Link)) {
sections.push('[Link]');
- sections.push(...this._formatLinkSection());
+ sections.push(...this._formatSection(this.Link));
}
// [Network] section
- if (this._hasNetworkValues()) {
+ if (this._hasSectionValues(this.Network)) {
sections.push('[Network]');
- sections.push(...this._formatNetworkSection());
+ sections.push(...this._formatSection(this.Network));
}
// [DHCP] section
- if (this._hasDHCPValues()) {
+ if (this._hasSectionValues(this.DHCP)) {
sections.push('[DHCP]');
- sections.push(...this._formatDHCPSection());
+ sections.push(...this._formatSection(this.DHCP));
}
// [Address] sections
- this.Address.forEach((addr, index) => {
- if (this._hasAddressValues(addr)) {
+ this.Address.forEach(addr => {
+ if (this._hasSectionValues(addr)) {
sections.push('[Address]');
- sections.push(...this._formatAddressSection(addr));
+ sections.push(...this._formatSection(addr));
}
});
// [Route] sections
- this.Route.forEach((route, index) => {
- if (this._hasRouteValues(route)) {
+ this.Route.forEach(route => {
+ if (this._hasSectionValues(route)) {
sections.push('[Route]');
- sections.push(...this._formatRouteSection(route));
+ sections.push(...this._formatSection(route));
}
});
return sections.join('\n') + '\n';
}
- // Helper methods to check if sections have values
- _hasMatchValues() {
- return Object.values(this.Match).some(val =>
- val && (Array.isArray(val) ? val.length > 0 : val !== '')
+ _hasSectionValues(section) {
+ return Object.values(section).some(field =>
+ field.value !== null && field.value !== undefined && field.value !== ''
);
}
- _hasLinkValues() {
- return Object.values(this.Link).some(val => val && val !== '');
- }
-
- _hasNetworkValues() {
- return Object.values(this.Network).some(val =>
- val && (Array.isArray(val) ? val.length > 0 : val !== '')
- );
- }
-
- _hasDHCPValues() {
- return Object.values(this.DHCP).some(val => val && val !== '');
- }
-
- _hasAddressValues(addr) {
- return Object.values(addr).some(val => val && val !== '');
- }
-
- _hasRouteValues(route) {
- return Object.values(route).some(val => val && val !== '');
- }
-
- // Formatting methods
- _formatMatchSection() {
+ _formatSection(section) {
const lines = [];
- const match = this.Match;
-
- if (match.MACAddress.length) lines.push(`MACAddress=${match.MACAddress.join(' ')}`);
- if (match.Name.length) lines.push(`Name=${match.Name.join(' ')}`);
- if (match.Driver.length) lines.push(`Driver=${match.Driver.join(' ')}`);
- if (match.Path.length) lines.push(`Path=${match.Path.join(' ')}`);
- if (match.Type.length) lines.push(`Type=${match.Type.join(' ')}`);
- if (match.Property) lines.push(`Property=${match.Property}`);
-
- return lines;
- }
-
- _formatLinkSection() {
- const lines = [];
- const link = this.Link;
-
- Object.entries(link).forEach(([key, value]) => {
- if (value && value !== '') lines.push(`${key}=${value}`);
- });
-
- return lines;
- }
-
- _formatNetworkSection() {
- const lines = [];
- const network = this.Network;
-
- if (network.Description) lines.push(`Description=${network.Description}`);
- if (network.DHCP.length) lines.push(`DHCP=${network.DHCP.join(' ')}`);
- if (network.DNS.length) lines.push(`DNS=${network.DNS.join(' ')}`);
- if (network.NTP.length) lines.push(`NTP=${network.NTP.join(' ')}`);
- if (network.IPForward.length) lines.push(`IPForward=${network.IPForward.join(' ')}`);
- if (network.IPv6PrivacyExtensions) lines.push(`IPv6PrivacyExtensions=${network.IPv6PrivacyExtensions}`);
- if (network.LLMNR) lines.push(`LLMNR=${network.LLMNR}`);
-
- return lines;
- }
-
- _formatDHCPSection() {
- const lines = [];
- const dhcp = this.DHCP;
-
- Object.entries(dhcp).forEach(([key, value]) => {
- if (value && value !== '') lines.push(`${key}=${value}`);
- });
-
- return lines;
- }
-
- _formatAddressSection(addr) {
- const lines = [];
-
- Object.entries(addr).forEach(([key, value]) => {
- if (value && value !== '') lines.push(`${key}=${value}`);
- });
-
- return lines;
- }
-
- _formatRouteSection(route) {
- const lines = [];
-
- Object.entries(route).forEach(([key, value]) => {
- if (value && value !== '') lines.push(`${key}=${value}`);
- });
-
+ for (const [key, field] of Object.entries(section)) {
+ if (field.value !== null && field.value !== undefined && field.value !== '') {
+ lines.push(`${key}=${field.toString()}`);
+ }
+ }
return lines;
}
}
// Export classes
export {
+ BaseField,
MACAddress,
IPv4Address,
IPv6Address,
+ BooleanYesNo,
+ Port,
+ MTU,
MatchSection,
LinkSection,
NetworkSection,