diff options
Diffstat (limited to 'static/structured-editor.js')
| -rw-r--r-- | static/structured-editor.js | 253 |
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 }; |
