summaryrefslogtreecommitdiffstats
path: root/static/structured-editor.js
diff options
context:
space:
mode:
Diffstat (limited to 'static/structured-editor.js')
-rw-r--r--static/structured-editor.js462
1 files changed, 225 insertions, 237 deletions
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 };
-