176 lines
6.9 KiB
Cheetah
176 lines
6.9 KiB
Cheetah
{{define "component"}}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
{{template "head" .}}
|
|
<body>
|
|
{{template "topbar" .}}
|
|
{{template "breadcrumbs" .}}
|
|
|
|
<main class="container">
|
|
<section class="card">
|
|
<div class="button-row" style="justify-content: space-between; margin-bottom: 16px;">
|
|
<h2 style="margin: 0;">Component Card</h2>
|
|
<div class="button-row">
|
|
<button class="button" id="component-edit-toggle" type="button">Edit</button>
|
|
<button class="button button-secondary" id="component-edit-cancel" type="button" hidden>Cancel</button>
|
|
</div>
|
|
</div>
|
|
<div class="meta" id="component-edit-message" style="margin-bottom: 12px;"></div>
|
|
<div class="meta-grid">
|
|
<div>
|
|
<span>Vendor Serial</span>
|
|
<div class="field-value">{{.Component.VendorSerial}}</div>
|
|
<input class="input field-input" id="component-vendor-serial-input" value="{{.Component.VendorSerial}}" hidden />
|
|
</div>
|
|
<div>
|
|
<span>Vendor</span>
|
|
<div class="field-value">{{if .Component.Vendor}}<a href="{{.ComponentVendorURL}}">{{.Component.Vendor}}</a>{{else}}—{{end}}</div>
|
|
<input class="input field-input" id="component-vendor-input" value="{{if .Component.Vendor}}{{.Component.Vendor}}{{end}}" hidden />
|
|
</div>
|
|
<div>
|
|
<span>Model</span>
|
|
<div class="field-value">{{if .Component.Model}}<a href="{{.ComponentModelURL}}">{{.Component.Model}}</a>{{else}}—{{end}}</div>
|
|
<input class="input field-input" id="component-model-input" value="{{if .Component.Model}}{{.Component.Model}}{{end}}" hidden />
|
|
</div>
|
|
<div><span>Status</span><span class="badge {{componentStatusClass .ComponentStatus}}">{{componentStatusText .ComponentStatus}}</span></div>
|
|
<div><span>Asset</span>{{if .CurrentAssetID}}<a href="{{.CurrentAssetURL}}">{{.CurrentAssetLabel}}</a>{{else}}—{{end}}</div>
|
|
<div><span>Firmware</span>{{if .ComponentFirmware}}<a href="{{.ComponentFirmwareURL}}">{{.ComponentFirmware}}</a>{{else}}—{{end}}</div>
|
|
<div><span>First Seen</span><span title="{{formatTimePtrFull .Component.FirstSeenAt}}">{{formatTimePtr .Component.FirstSeenAt}}</span></div>
|
|
<div><span>Created</span><span title="{{formatTimeFull .Component.CreatedAt}}">{{formatTime .Component.CreatedAt}}</span></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Installation History</h2>
|
|
{{if .InstallationHistory}}
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Asset</th>
|
|
<th>Installed At</th>
|
|
<th>Removed At</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .InstallationHistory}}
|
|
<tr>
|
|
<td><a href="{{.AssetURL}}">{{.AssetLabel}}</a></td>
|
|
<td>{{formatTimeFull .InstalledAt}}</td>
|
|
<td>{{if .RemovedAt}}{{formatTimePtrFull .RemovedAt}}{{else}}—{{end}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{else}}
|
|
<div class="meta">No installation history.</div>
|
|
{{end}}
|
|
</section>
|
|
|
|
<section class="card">
|
|
<h2>Timeline</h2>
|
|
<div id="component-timeline-panel-{{.Component.ID}}" class="timeline-panel"></div>
|
|
<div class="meta">Timeline groups repeated events and lets you drill down to individual history entries.</div>
|
|
</section>
|
|
</main>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
initTimelinePanel({
|
|
rootId: 'component-timeline-panel-{{.Component.ID}}',
|
|
apiBase: '/api/history/components/{{.Component.ID}}/timeline',
|
|
detailBase: '/api/history/components/{{.Component.ID}}/events'
|
|
});
|
|
});
|
|
</script>
|
|
<script>
|
|
const componentEditToggle = document.getElementById('component-edit-toggle');
|
|
const componentEditCancel = document.getElementById('component-edit-cancel');
|
|
const componentEditMessage = document.getElementById('component-edit-message');
|
|
const componentFieldValues = [...document.querySelectorAll('.field-value')];
|
|
const componentFieldInputs = [...document.querySelectorAll('.field-input')];
|
|
const componentInputs = {
|
|
vendorSerial: document.getElementById('component-vendor-serial-input'),
|
|
vendor: document.getElementById('component-vendor-input'),
|
|
model: document.getElementById('component-model-input')
|
|
};
|
|
const initialComponentForm = {
|
|
vendorSerial: componentInputs.vendorSerial ? componentInputs.vendorSerial.value : '',
|
|
vendor: componentInputs.vendor ? componentInputs.vendor.value : '',
|
|
model: componentInputs.model ? componentInputs.model.value : ''
|
|
};
|
|
let componentEditMode = false;
|
|
|
|
function setComponentMessage(message) {
|
|
if (componentEditMessage) {
|
|
componentEditMessage.textContent = message;
|
|
}
|
|
}
|
|
|
|
function resetComponentForm() {
|
|
if (componentInputs.vendorSerial) componentInputs.vendorSerial.value = initialComponentForm.vendorSerial;
|
|
if (componentInputs.vendor) componentInputs.vendor.value = initialComponentForm.vendor;
|
|
if (componentInputs.model) componentInputs.model.value = initialComponentForm.model;
|
|
}
|
|
|
|
function setComponentEditMode(enabled) {
|
|
componentEditMode = enabled;
|
|
componentFieldValues.forEach((element) => {
|
|
element.hidden = enabled;
|
|
});
|
|
componentFieldInputs.forEach((element) => {
|
|
element.hidden = !enabled;
|
|
});
|
|
if (componentEditToggle) {
|
|
componentEditToggle.textContent = enabled ? 'Save' : 'Edit';
|
|
}
|
|
if (componentEditCancel) {
|
|
componentEditCancel.hidden = !enabled;
|
|
}
|
|
if (!enabled) {
|
|
setComponentMessage('');
|
|
}
|
|
}
|
|
|
|
async function saveComponent() {
|
|
const payload = {
|
|
vendor_serial: (componentInputs.vendorSerial ? componentInputs.vendorSerial.value : '').trim(),
|
|
vendor: (componentInputs.vendor ? componentInputs.vendor.value : '').trim(),
|
|
model: (componentInputs.model ? componentInputs.model.value : '').trim()
|
|
};
|
|
if (!payload.vendor_serial) {
|
|
setComponentMessage('Vendor serial is required.');
|
|
return;
|
|
}
|
|
const response = await fetch('/registry/components/{{.Component.ID}}', {
|
|
method: 'PUT',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
if (!response.ok) {
|
|
const body = await response.json().catch(() => ({error: 'Request failed'}));
|
|
setComponentMessage(body.error || 'Update component failed');
|
|
return;
|
|
}
|
|
window.location.reload();
|
|
}
|
|
|
|
if (componentEditToggle) {
|
|
componentEditToggle.addEventListener('click', async () => {
|
|
if (!componentEditMode) {
|
|
setComponentEditMode(true);
|
|
return;
|
|
}
|
|
await saveComponent();
|
|
});
|
|
}
|
|
|
|
if (componentEditCancel) {
|
|
componentEditCancel.addEventListener('click', () => {
|
|
resetComponentForm();
|
|
setComponentEditMode(false);
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|