/* jshint esversion: 2024, module: true */
/**
* Structured Editor for systemd-networkd configuration
* @class StructuredEditor
*/
class StructuredEditor {
constructor(container) {
this.container = container;
this.schema = null;
this.currentFile = "";
}
/**
* Load configuration schema
* @param {Object} schema - Configuration schema from NetworkConfiguration.getSchema()
* @param {string} filename - File name
*/
loadSchema(schema, filename) {
if (!this.container) {
console.error("Structured editor container not found");
return;
}
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.container) {
console.error("Cannot render: container is null");
return;
}
if (!this.schema) {
this.container.innerHTML =
'
No configuration schema loaded
';
return;
}
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 from schema
* @private
* @returns {string}
*/
_createEditorHTML() {
return `
${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)}
`;
}
/**
* 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 `
[${sectionName}]
${fieldsHTML}
`;
}
/**
* 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 `
[${sectionName}]
No ${sectionName.toLowerCase()} sections
`;
}
const sectionsHTML = arraySchema.items
.map((itemSchema, index) => {
const fieldsHTML = Object.entries(itemSchema)
.map(([key, field]) =>
this._createFieldInput(key, field, sectionName, index),
)
.join("");
return `
[${sectionName}] ${index > 0 ? `#${index + 1}` : ""}
${fieldsHTML}
`;
})
.join("");
return `
${sectionsHTML}
`;
}
/**
* 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}"`);
}
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) =>
``,
)
.join("");
return `
`;
} else {
// Create text input for other fields
return `
`;
}
}
/**
* Check if section has any values
* @private
* @param {Object} sectionSchema - Section schema
* @returns {boolean}
*/
_hasSectionValues(sectionSchema) {
return Object.values(sectionSchema).some(
(field) =>
field.value !== null && field.value !== undefined && field.value !== "",
);
}
/**
* 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 section buttons
this.container.querySelectorAll("[data-add-section]").forEach((btn) => {
btn.addEventListener("click", (e) => this._onAddSection(e));
});
// Remove section buttons
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;
this._updateFieldValue(input);
}
/**
* Handle select changes
* @private
* @param {Event} event
*/
_onSelectChange(event) {
const select = event.target;
this._updateFieldValue(select);
}
/**
* Update field value in schema
* @private
* @param {HTMLElement} element - Input or select element
*/
_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;
}
}
}
/**
* Handle add section
* @private
* @param {Event} event
*/
_onAddSection(event) {
const sectionName = event.target.dataset.addSection;
this.emit("addSection", { section: sectionName });
}
/**
* Handle remove section
* @private
* @param {Event} event
*/
_onRemoveSection(event) {
const section = event.target.dataset.section;
const index = parseInt(event.target.dataset.index);
this.emit("removeSection", { section, index });
}
/**
* Show error message
* @param {string} message - Error message
*/
showError(message) {
if (this.container) {
this.container.innerHTML = `${message}
`;
}
}
/**
* 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);
}
/**
* Add event listener
* @param {string} eventName - Event name
* @param {Function} callback - Event callback
*/
on(eventName, callback) {
this.container.addEventListener(eventName, callback);
}
/**
* Get current schema
* @returns {Object|null}
*/
getSchema() {
return this.schema;
}
}
export { StructuredEditor };