351 lines
13 KiB
JavaScript
351 lines
13 KiB
JavaScript
// Firmware Form Component for add/edit operations in drawer
|
|
class FirmwareFormComponent extends Component {
|
|
constructor(container, viewModel, eventBus) {
|
|
super(container, viewModel, eventBus);
|
|
|
|
this.firmwareData = null;
|
|
this.onSaveCallback = null;
|
|
this.onCancelCallback = null;
|
|
this.isEditMode = false;
|
|
}
|
|
|
|
setFirmwareData(firmwareData) {
|
|
this.firmwareData = firmwareData;
|
|
this.isEditMode = !!firmwareData;
|
|
}
|
|
|
|
setOnSaveCallback(callback) {
|
|
this.onSaveCallback = callback;
|
|
}
|
|
|
|
setOnCancelCallback(callback) {
|
|
this.onCancelCallback = callback;
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Submit button
|
|
const submitBtn = this.findElement('button[type="submit"]');
|
|
if (submitBtn) {
|
|
this.addEventListener(submitBtn, 'click', this.handleSubmit.bind(this));
|
|
}
|
|
|
|
// Cancel button
|
|
const cancelBtn = this.findElement('#cancel-btn');
|
|
if (cancelBtn) {
|
|
this.addEventListener(cancelBtn, 'click', this.handleCancel.bind(this));
|
|
}
|
|
|
|
// File input
|
|
const fileInput = this.findElement('#firmware-file');
|
|
if (fileInput) {
|
|
this.addEventListener(fileInput, 'change', this.handleFileSelect.bind(this));
|
|
}
|
|
|
|
// Labels management
|
|
this.setupLabelsManagement();
|
|
}
|
|
|
|
setupLabelsManagement() {
|
|
// Add label button
|
|
const addLabelBtn = this.findElement('#add-label-btn');
|
|
if (addLabelBtn) {
|
|
this.addEventListener(addLabelBtn, 'click', this.addLabel.bind(this));
|
|
}
|
|
|
|
// Remove label buttons (delegated event handling)
|
|
const labelsContainer = this.findElement('#labels-container');
|
|
if (labelsContainer) {
|
|
this.addEventListener(labelsContainer, 'click', (e) => {
|
|
const removeBtn = e.target.closest('.remove-label-btn');
|
|
if (removeBtn) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.stopImmediatePropagation();
|
|
|
|
const key = removeBtn.getAttribute('data-label-key');
|
|
if (key) {
|
|
this.removeLabel(key);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
mount() {
|
|
super.mount();
|
|
this.render();
|
|
this.setupEventListeners();
|
|
}
|
|
|
|
render() {
|
|
const container = this.container;
|
|
if (!container) return;
|
|
|
|
const labels = this.firmwareData?.labels || {};
|
|
const labelsHTML = Object.entries(labels).map(([key, value]) =>
|
|
`<div class="label-item">
|
|
<span class="label-key">${this.escapeHtml(key)}</span>
|
|
<span class="label-value">${this.escapeHtml(value)}</span>
|
|
<button type="button" class="remove-label-btn" data-label-key="${this.escapeHtml(key)}" title="Remove label">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
</div>`
|
|
).join('');
|
|
|
|
container.innerHTML = `
|
|
<div class="firmware-form">
|
|
<div class="form-group">
|
|
<label for="firmware-name">Name *</label>
|
|
<input
|
|
type="text"
|
|
id="firmware-name"
|
|
name="name"
|
|
value="${this.firmwareData?.name || ''}"
|
|
placeholder="e.g., base, neopattern, relay"
|
|
${this.isEditMode ? 'readonly' : ''}
|
|
>
|
|
<small class="form-help">${this.isEditMode ? 'Name cannot be changed after creation' : 'Unique identifier for the firmware'}</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="firmware-version">Version *</label>
|
|
<input
|
|
type="text"
|
|
id="firmware-version"
|
|
name="version"
|
|
value="${this.firmwareData?.version || ''}"
|
|
placeholder="e.g., 1.0.0, 2.1.3"
|
|
${this.isEditMode ? 'readonly' : ''}
|
|
>
|
|
<small class="form-help">${this.isEditMode ? 'Version cannot be changed after creation' : 'Semantic version (e.g., 1.0.0)'}</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="firmware-file">Firmware File *</label>
|
|
<div class="file-input-wrapper">
|
|
<input
|
|
type="file"
|
|
id="firmware-file"
|
|
name="firmware"
|
|
accept=".bin,.hex"
|
|
>
|
|
<label for="firmware-file" class="file-input-label">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
|
<path d="M14 2v6h6"/>
|
|
</svg>
|
|
<span id="file-name">${this.isEditMode ? 'Current file: ' + (this.firmwareData?.name || 'unknown') + '.bin' : 'Choose firmware file...'}</span>
|
|
</label>
|
|
</div>
|
|
<small class="form-help">${this.isEditMode ? 'Select a new firmware file to update, or leave empty to update metadata only' : 'Binary firmware file (.bin or .hex)'}</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Labels</label>
|
|
<div class="labels-section">
|
|
<div class="add-label-controls">
|
|
<input type="text" id="label-key" placeholder="Key" class="label-key-input">
|
|
<span class="label-separator">:</span>
|
|
<input type="text" id="label-value" placeholder="Value" class="label-value-input">
|
|
<button type="button" id="add-label-btn" class="add-label-btn">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
<line x1="12" y1="5" x2="12" y2="19"/>
|
|
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
</svg>
|
|
Add Label
|
|
</button>
|
|
</div>
|
|
<div id="labels-container" class="labels-container">
|
|
${labelsHTML}
|
|
</div>
|
|
</div>
|
|
<small class="form-help">Key-value pairs for categorizing firmware (e.g., platform: esp32, app: base)</small>
|
|
</div>
|
|
|
|
<div class="firmware-actions">
|
|
<button type="button" id="cancel-btn" class="config-btn">Cancel</button>
|
|
<button type="submit" class="config-btn">
|
|
${this.isEditMode ? 'Update Firmware' : 'Upload Firmware'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
handleFileSelect(event) {
|
|
const file = event.target.files[0];
|
|
const fileNameSpan = this.findElement('#file-name');
|
|
|
|
if (file) {
|
|
fileNameSpan.textContent = file.name;
|
|
} else {
|
|
fileNameSpan.textContent = this.isEditMode ?
|
|
'Current file: ' + (this.firmwareData?.name || 'unknown') + '.bin' :
|
|
'Choose firmware file...';
|
|
}
|
|
}
|
|
|
|
addLabel() {
|
|
const keyInput = this.findElement('#label-key');
|
|
const valueInput = this.findElement('#label-value');
|
|
const labelsContainer = this.findElement('#labels-container');
|
|
|
|
const key = keyInput.value.trim();
|
|
const value = valueInput.value.trim();
|
|
|
|
if (!key || !value) {
|
|
this.showError('Please enter both key and value for the label');
|
|
return;
|
|
}
|
|
|
|
// Check if key already exists
|
|
const existingLabel = labelsContainer.querySelector(`[data-label-key="${this.escapeHtml(key)}"]`);
|
|
if (existingLabel) {
|
|
this.showError('A label with this key already exists');
|
|
return;
|
|
}
|
|
|
|
// Add the label
|
|
const labelHTML = `
|
|
<div class="label-item">
|
|
<span class="label-key">${this.escapeHtml(key)}</span>
|
|
<span class="label-value">${this.escapeHtml(value)}</span>
|
|
<button type="button" class="remove-label-btn" data-label-key="${this.escapeHtml(key)}" title="Remove label">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
labelsContainer.insertAdjacentHTML('beforeend', labelHTML);
|
|
|
|
// Clear inputs
|
|
keyInput.value = '';
|
|
valueInput.value = '';
|
|
}
|
|
|
|
removeLabel(key) {
|
|
const removeBtn = this.findElement(`.remove-label-btn[data-label-key="${this.escapeHtml(key)}"]`);
|
|
if (removeBtn) {
|
|
const labelItem = removeBtn.closest('.label-item');
|
|
if (labelItem) {
|
|
labelItem.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
async handleSubmit(event) {
|
|
event.preventDefault();
|
|
|
|
try {
|
|
const nameInput = this.findElement('#firmware-name');
|
|
const versionInput = this.findElement('#firmware-version');
|
|
const firmwareFile = this.findElement('#firmware-file').files[0];
|
|
|
|
const name = nameInput.value.trim();
|
|
const version = versionInput.value.trim();
|
|
|
|
if (!name || !version) {
|
|
this.showError('Name and version are required');
|
|
return;
|
|
}
|
|
|
|
// Only require file for new uploads, not for edit mode when keeping existing file
|
|
if (!this.isEditMode && (!firmwareFile || firmwareFile.size === 0)) {
|
|
this.showError('Please select a firmware file');
|
|
return;
|
|
}
|
|
|
|
// Collect labels
|
|
const labels = {};
|
|
const labelItems = this.findAllElements('.label-item');
|
|
labelItems.forEach(item => {
|
|
const key = item.querySelector('.label-key').textContent;
|
|
const value = item.querySelector('.label-value').textContent;
|
|
labels[key] = value;
|
|
});
|
|
|
|
// Prepare metadata
|
|
const metadata = {
|
|
name,
|
|
version,
|
|
labels
|
|
};
|
|
|
|
// Handle upload vs metadata-only update
|
|
if (this.isEditMode && (!firmwareFile || firmwareFile.size === 0)) {
|
|
// Metadata-only update
|
|
await window.apiClient.updateFirmwareMetadata(name, version, metadata);
|
|
} else {
|
|
// Full upload (new firmware or edit with new file)
|
|
await window.apiClient.uploadFirmwareToRegistry(metadata, firmwareFile);
|
|
}
|
|
|
|
this.showSuccess(this.isEditMode ? 'Firmware updated successfully' : 'Firmware uploaded successfully');
|
|
|
|
if (this.onSaveCallback) {
|
|
this.onSaveCallback();
|
|
}
|
|
|
|
} catch (error) {
|
|
logger.error('Firmware upload failed:', error);
|
|
this.showError('Upload failed: ' + error.message);
|
|
}
|
|
}
|
|
|
|
handleCancel() {
|
|
if (this.onCancelCallback) {
|
|
this.onCancelCallback();
|
|
}
|
|
}
|
|
|
|
showError(message) {
|
|
this.showNotification(message, 'error');
|
|
}
|
|
|
|
showSuccess(message) {
|
|
this.showNotification(message, 'success');
|
|
}
|
|
|
|
showNotification(message, type = 'info') {
|
|
// Remove any existing notifications
|
|
const existing = this.findElement('.form-notification');
|
|
if (existing) {
|
|
existing.remove();
|
|
}
|
|
|
|
const notification = document.createElement('div');
|
|
notification.className = `form-notification notification-${type}`;
|
|
notification.textContent = message;
|
|
|
|
this.container.insertBefore(notification, this.container.firstChild);
|
|
|
|
setTimeout(() => {
|
|
notification.classList.add('show');
|
|
}, 100);
|
|
|
|
setTimeout(() => {
|
|
notification.classList.remove('show');
|
|
setTimeout(() => {
|
|
if (notification.parentNode) {
|
|
notification.remove();
|
|
}
|
|
}, 300);
|
|
}, 3000);
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
if (typeof text !== 'string') return text;
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
window.FirmwareFormComponent = FirmwareFormComponent;
|