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.js253
1 files changed, 253 insertions, 0 deletions
diff --git a/static/structured-editor.js b/static/structured-editor.js
new file mode 100644
index 0000000..dc46bef
--- /dev/null
+++ b/static/structured-editor.js
@@ -0,0 +1,253 @@
+/* jshint esversion: 2024, module: true */
+
+/**
+ * Structured Editor for systemd-networkd configuration
+ * @module StructuredEditor
+ */
+
+import { NetworkConfiguration } from './systemd-network.js';
+
+class StructuredEditor {
+ constructor(container) {
+ this.container = container;
+ this.config = new NetworkConfiguration();
+ this.currentFile = '';
+ }
+
+ /**
+ * Load configuration from text
+ * @param {string} configText - Configuration text
+ * @param {string} filename - File name
+ */
+ loadConfiguration(configText, filename) {
+ this.config = NetworkConfiguration.fromSystemdConfiguration(configText);
+ this.currentFile = filename;
+ this.render();
+ }
+
+ /**
+ * Render the structured editor
+ */
+ render() {
+ this.container.innerHTML = this._createEditorHTML();
+ this._attachEventListeners();
+ }
+
+ /**
+ * Create editor HTML structure
+ * @private
+ * @returns {string}
+ */
+ _createEditorHTML() {
+ 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" id="addAddressSection">Add Address</button>
+ <button class="button" id="addRouteSection">Add Route</button>
+ <button class="button secondary" id="showRawConfig">Show Raw</button>
+ </div>
+ </div>
+ `;
+ }
+
+ _createMatchSection() {
+ const match = this.config.Match;
+ return `
+ <div class="config-section">
+ <h4>[Match]</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')}
+ </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')}
+ </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')}
+ </div>
+ </div>
+ `;
+ }
+
+ _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>
+ </div>
+ `;
+ }
+
+ _createAddressSections() {
+ 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 remove-section" data-type="address" data-index="${index}">Remove</button>
+ </div>
+ </div>
+ `).join('');
+ }
+
+ _createRouteSections() {
+ 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 remove-section" data-type="route" data-index="${index}">Remove</button>
+ </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-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-key="${key}">
+ ${optionsHTML}
+ </select>
+ </div>
+ `;
+ }
+
+ _attachEventListeners() {
+ // Input changes
+ this.container.querySelectorAll('.config-input').forEach(input => {
+ input.addEventListener('change', (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.config.Address.push(new (await import('./systemd-network.js')).AddressSection());
+ this.render();
+ });
+
+ this.container.querySelector('#addRouteSection')?.addEventListener('click', () => {
+ this.config.Route.push(new (await import('./systemd-network.js')).RouteSection());
+ this.render();
+ });
+
+ // Remove sections
+ this.container.querySelectorAll('.remove-section').forEach(btn => {
+ btn.addEventListener('click', (e) => this._onRemoveSection(e));
+ });
+ }
+
+ _onInputChange(event) {
+ const input = event.target;
+ const key = input.dataset.key;
+ const value = input.value;
+
+ // Update configuration based on context
+ this._updateConfigValue(key, value);
+ }
+
+ _onSelectChange(event) {
+ const select = event.target;
+ const key = select.dataset.key;
+ const value = select.value;
+
+ this._updateConfigValue(key, value);
+ }
+
+ _updateConfigValue(key, value) {
+ // This would need to be implemented based on the current section context
+ console.log(`Update ${key} = ${value}`);
+ // Implementation would update the this.config object
+ }
+
+ _onRemoveSection(event) {
+ const btn = event.target;
+ const type = btn.dataset.type;
+ const index = parseInt(btn.dataset.index);
+
+ if (type === 'address') {
+ this.config.Address.splice(index, 1);
+ } else if (type === 'route') {
+ this.config.Route.splice(index, 1);
+ }
+
+ this.render();
+ }
+
+ /**
+ * Get current configuration as text
+ * @returns {string}
+ */
+ getConfigurationText() {
+ return this.config.toSystemdConfiguration();
+ }
+}
+
+export { StructuredEditor };