Rework FRU and DMI editors: per-row inline save, all fields editable
- Replace global Save button with per-row ✓ (save) / ✗ (cancel) buttons that appear only when a field is changed - All fields shown as editable inputs; server rejects unknown fields with a clear error message instead of hiding them in the UI - Monospace font and 1.5px border for all value inputs - Server-side name→area/index lookup for fields sent without area - SAA DMI card: same per-row UX, confirm dialog kept (requires reboot) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -213,98 +213,85 @@ func runSAADMIWriteTask(ctx context.Context, j *jobState, exportDir string, p ta
|
||||
}
|
||||
|
||||
func renderSAADMICard() string {
|
||||
return `<div class="card"><div class="card-head">Supermicro — DMI <button class="btn btn-sm btn-secondary" onclick="saaDMIRead()" style="margin-left:auto">Read</button></div><div class="card-body">
|
||||
return `<div class="card"><div class="card-head card-head-actions">Supermicro — DMI<div class="card-head-buttons"><button class="btn btn-sm btn-secondary" onclick="saaDMIRead()">Read</button></div></div><div class="card-body">
|
||||
<p style="font-size:13px;color:var(--muted);margin-bottom:12px">Reads and edits DMI fields via SAA (In-Band).</p>
|
||||
<div id="saa-dmi-status" style="font-size:13px;color:var(--muted);margin-bottom:8px"></div>
|
||||
<div id="saa-dmi-table"></div>
|
||||
<div id="saa-dmi-save-row" style="display:none;margin-top:12px">
|
||||
<button class="btn btn-primary" id="saa-dmi-save-btn" onclick="saaDMISave()">Save</button>
|
||||
<span id="saa-dmi-save-msg" style="font-size:13px;color:var(--muted);margin-left:10px"></span>
|
||||
</div>
|
||||
</div></div>
|
||||
<script>
|
||||
function saaDMIEsc(s) {
|
||||
return String(s==null?'':s).replace(/[&<>"']/g,function(c){return{'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];});
|
||||
}
|
||||
function saaDMIUpdateSaveBtn() {
|
||||
var inputs = document.querySelectorAll('#saa-dmi-table input[data-original]');
|
||||
var dirty = [];
|
||||
inputs.forEach(function(inp){if(inp.value!==inp.dataset.original)dirty.push(inp);});
|
||||
var row = document.getElementById('saa-dmi-save-row');
|
||||
var btn = document.getElementById('saa-dmi-save-btn');
|
||||
if(dirty.length>0){row.style.display='';btn.textContent='Save ('+dirty.length+' changed)';}
|
||||
else{row.style.display='none';}
|
||||
}
|
||||
var _dmiActBtnStyle = 'width:22px;height:22px;padding:0;font-size:13px;line-height:1;border:1px solid var(--line);border-radius:3px;background:var(--surface);cursor:pointer;vertical-align:middle;';
|
||||
var _dmiInputStyle = 'width:100%;padding:3px 6px;border:1.5px solid #888;border-radius:3px;font-size:13px;font-family:monospace;background:var(--surface);color:var(--ink);';
|
||||
function dmiEsc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||||
function saaDMIRead() {
|
||||
var status = document.getElementById('saa-dmi-status');
|
||||
var table = document.getElementById('saa-dmi-table');
|
||||
var saveRow = document.getElementById('saa-dmi-save-row');
|
||||
status.textContent = 'Reading...';
|
||||
status.style.color = 'var(--muted)';
|
||||
table.innerHTML = '';
|
||||
saveRow.style.display = 'none';
|
||||
fetch('/api/tools/saa-dmi').then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||('HTTP '+r.status));return d;});}).then(function(fields){
|
||||
status.textContent = fields.length+' field(s) loaded.';
|
||||
var rows = fields.map(function(f){
|
||||
return '<tr>'
|
||||
+'<td style="font-size:13px;white-space:nowrap;padding-right:8px">'+saaDMIEsc(f.name)+'</td>'
|
||||
+'<td style="font-family:monospace;font-size:13px;white-space:nowrap;padding-right:8px">'+saaDMIEsc(f.shn)+'</td>'
|
||||
+'<td><input type="text" value="'+saaDMIEsc(f.value)+'" data-shn="'+saaDMIEsc(f.shn)+'" data-original="'+saaDMIEsc(f.value)+'" oninput="saaDMIMarkDirty(this)" style="width:100%;font-family:monospace;font-size:13px;border:1px solid var(--line);padding:3px 6px;border-radius:3px"></td>'
|
||||
+'<td id="saa-dmi-dirty-'+saaDMIEsc(f.shn)+'" style="font-size:12px;color:var(--warn,#b45309);width:50px;padding-left:6px"></td>'
|
||||
+'</tr>';
|
||||
}).join('');
|
||||
table.innerHTML = '<table style="width:100%;border-collapse:collapse"><tr><th style="text-align:left;font-size:13px;padding-bottom:6px">Field</th><th style="text-align:left;font-size:13px;padding-bottom:6px">Shn</th><th style="text-align:left;font-size:13px;padding-bottom:6px">Value</th><th></th></tr>'+rows+'</table>';
|
||||
}).catch(function(e){
|
||||
status.textContent = 'Error: '+e.message;
|
||||
status.style.color = 'var(--crit-fg,#9f3a38)';
|
||||
});
|
||||
status.textContent = 'Reading...'; status.style.color = 'var(--muted)';
|
||||
document.getElementById('saa-dmi-table').innerHTML = '';
|
||||
fetch('/api/tools/saa-dmi', {cache:'no-store'})
|
||||
.then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||('HTTP '+r.status));return d;});})
|
||||
.then(function(fields){
|
||||
status.textContent = fields.length + ' field(s) loaded.';
|
||||
var rows = fields.map(function(f){
|
||||
var val = dmiEsc(f.value||'');
|
||||
return '<tr>'
|
||||
+ '<td style="font-size:13px;color:var(--muted);white-space:nowrap;padding-right:8px;vertical-align:middle">'+dmiEsc(f.name)+'</td>'
|
||||
+ '<td style="font-family:monospace;font-size:12px;color:var(--muted);white-space:nowrap;padding-right:8px;vertical-align:middle">'+dmiEsc(f.shn)+'</td>'
|
||||
+ '<td style="vertical-align:middle"><input class="dmi-inp" type="text" style="'+_dmiInputStyle+'"'
|
||||
+ ' data-shn="'+dmiEsc(f.shn)+'" data-original="'+val+'" value="'+val+'" oninput="dmiChanged(this)"></td>'
|
||||
+ '<td class="dmi-act" style="display:none;white-space:nowrap;padding-left:6px;vertical-align:middle">'
|
||||
+ '<button style="'+_dmiActBtnStyle+'color:var(--ok-fg,green);margin-right:3px" title="Save" onclick="dmiSave(this)">✓</button>'
|
||||
+ '<button style="'+_dmiActBtnStyle+'color:var(--crit-fg,#9f3a38)" title="Cancel" onclick="dmiCancel(this)">✗</button>'
|
||||
+ '<span class="dmi-msg" style="font-size:11px;margin-left:5px;color:var(--muted)"></span>'
|
||||
+ '</td></tr>';
|
||||
}).join('');
|
||||
document.getElementById('saa-dmi-table').innerHTML =
|
||||
'<table style="width:100%;border-collapse:collapse">'
|
||||
+ '<tr><th style="text-align:left;font-size:12px;color:var(--muted);padding-bottom:6px;font-weight:normal">Field</th>'
|
||||
+ '<th style="text-align:left;font-size:12px;color:var(--muted);padding-bottom:6px;font-weight:normal">SHN</th>'
|
||||
+ '<th style="text-align:left;font-size:12px;color:var(--muted);padding-bottom:6px;font-weight:normal">Value</th><th></th></tr>'
|
||||
+ rows + '</table>';
|
||||
})
|
||||
.catch(function(e){ status.textContent='Error: '+e.message; status.style.color='var(--crit-fg,#9f3a38)'; });
|
||||
}
|
||||
function saaDMIMarkDirty(inp) {
|
||||
var shn = inp.dataset.shn;
|
||||
var cell = document.getElementById('saa-dmi-dirty-'+shn);
|
||||
if(cell)cell.textContent = inp.value!==inp.dataset.original?'changed':'';
|
||||
saaDMIUpdateSaveBtn();
|
||||
function dmiChanged(inp) {
|
||||
inp.closest('tr').querySelector('.dmi-act').style.display = inp.value !== inp.dataset.original ? '' : 'none';
|
||||
}
|
||||
function saaDMIWaitTask(taskID) {
|
||||
var msg = document.getElementById('saa-dmi-save-msg');
|
||||
msg.textContent = 'Task '+taskID+' queued...';
|
||||
msg.style.color = 'var(--muted)';
|
||||
var timer = setInterval(function(){
|
||||
fetch('/api/tasks').then(function(r){return r.json();}).then(function(tasks){
|
||||
var task = (tasks||[]).find(function(t){return t.id===taskID;});
|
||||
if(!task)return;
|
||||
if(task.status==='done'||task.status==='failed'||task.status==='cancelled'){
|
||||
clearInterval(timer);
|
||||
msg.textContent = task.status==='done'?'Saved. Reboot to apply.':'Failed: '+(task.error||task.status);
|
||||
msg.style.color = task.status==='done'?'var(--ok,green)':'var(--crit-fg,#9f3a38)';
|
||||
document.getElementById('saa-dmi-save-btn').disabled = false;
|
||||
}
|
||||
}).catch(function(){});
|
||||
}, 1500);
|
||||
function dmiCancel(btn) {
|
||||
var row = btn.closest('tr');
|
||||
var inp = row.querySelector('.dmi-inp');
|
||||
inp.value = inp.dataset.original;
|
||||
row.querySelector('.dmi-act').style.display = 'none';
|
||||
row.querySelector('.dmi-msg').textContent = '';
|
||||
}
|
||||
function saaDMISave() {
|
||||
var inputs = document.querySelectorAll('#saa-dmi-table input[data-original]');
|
||||
var changes = [];
|
||||
inputs.forEach(function(inp){if(inp.value!==inp.dataset.original)changes.push({shn:inp.dataset.shn,value:inp.value});});
|
||||
if(!changes.length)return;
|
||||
var names = changes.map(function(c){return c.shn;}).join(', ');
|
||||
if(!window.confirm('Apply DMI changes for: '+names+'?\n\nThe server will need to be rebooted for changes to take effect.'))return;
|
||||
var btn = document.getElementById('saa-dmi-save-btn');
|
||||
var msg = document.getElementById('saa-dmi-save-msg');
|
||||
btn.disabled = true;
|
||||
msg.textContent = 'Submitting...';
|
||||
msg.style.color = 'var(--muted)';
|
||||
fetch('/api/tools/saa-dmi/write',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({changes:changes})
|
||||
}).then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||('HTTP '+r.status));return d;});}).then(function(d){
|
||||
saaDMIWaitTask(d.task_id);
|
||||
}).catch(function(e){
|
||||
msg.textContent = 'Error: '+e.message;
|
||||
msg.style.color = 'var(--crit-fg,#9f3a38)';
|
||||
btn.disabled = false;
|
||||
});
|
||||
function dmiSave(btn) {
|
||||
var row = btn.closest('tr');
|
||||
var inp = row.querySelector('.dmi-inp');
|
||||
var msg = row.querySelector('.dmi-msg');
|
||||
var cancelBtn = row.querySelectorAll('.dmi-act button')[1];
|
||||
if(!window.confirm('Apply DMI change for '+inp.dataset.shn+'?\nServer will need to reboot for changes to take effect.'))return;
|
||||
btn.disabled=true; cancelBtn.disabled=true;
|
||||
msg.textContent='…'; msg.style.color='var(--muted)';
|
||||
fetch('/api/tools/saa-dmi/write',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({changes:[{shn:inp.dataset.shn,value:inp.value}]})})
|
||||
.then(function(r){return r.json().then(function(d){if(!r.ok)throw new Error(d.error||('HTTP '+r.status));return d;});})
|
||||
.then(function(d){
|
||||
var poll=setInterval(function(){
|
||||
fetch('/api/tasks',{cache:'no-store'}).then(function(r){return r.json();}).then(function(tasks){
|
||||
var t=(tasks||[]).find(function(x){return x.id===d.task_id;});
|
||||
if(!t)return;
|
||||
if(t.status==='done'){
|
||||
clearInterval(poll);
|
||||
inp.dataset.original=inp.value;
|
||||
row.querySelector('.dmi-act').style.display='none';
|
||||
msg.textContent='Saved. Reboot to apply.'; msg.style.color='var(--ok-fg,green)';
|
||||
} else if(t.status==='failed'||t.status==='cancelled'){
|
||||
clearInterval(poll);
|
||||
msg.textContent=t.error||t.status; msg.style.color='var(--crit-fg)';
|
||||
btn.disabled=false; cancelBtn.disabled=false;
|
||||
}
|
||||
});
|
||||
},1500);
|
||||
})
|
||||
.catch(function(e){msg.textContent='Error: '+e.message; msg.style.color='var(--crit-fg)'; btn.disabled=false; cancelBtn.disabled=false;});
|
||||
}
|
||||
</script>
|
||||
</div></div>`
|
||||
</script>`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user