Files
core/internal/api/ui_component.tmpl

205 lines
8.8 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">
<div class="button-row" style="justify-content: space-between; margin-bottom: 12px;">
<h2 style="margin:0;">Activity</h2>
<div class="button-row">
<button class="button" id="component-activity-tab-timeline" type="button">Movement & Events</button>
<button class="button button-secondary" id="component-activity-tab-imports" type="button">Import History</button>
</div>
</div>
<div id="component-activity-view-timeline">
<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>
</div>
<div id="component-activity-view-imports" hidden>
<div id="component-imports-panel-{{.Component.ID}}" class="timeline-panel"></div>
<div class="meta">Import history shows upload type, upload time, and import metadata.</div>
</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'
});
initImportHistoryPanel({
rootId: 'component-imports-panel-{{.Component.ID}}',
apiBase: '/components/{{.Component.ID}}/imports'
});
const tabTimeline = document.getElementById('component-activity-tab-timeline');
const tabImports = document.getElementById('component-activity-tab-imports');
const viewTimeline = document.getElementById('component-activity-view-timeline');
const viewImports = document.getElementById('component-activity-view-imports');
function setComponentActivityTab(tab) {
const timelineActive = tab === 'timeline';
if (viewTimeline) viewTimeline.hidden = !timelineActive;
if (viewImports) viewImports.hidden = timelineActive;
if (tabTimeline) tabTimeline.className = timelineActive ? 'button' : 'button button-secondary';
if (tabImports) tabImports.className = timelineActive ? 'button button-secondary' : 'button';
}
if (tabTimeline) tabTimeline.addEventListener('click', () => setComponentActivityTab('timeline'));
if (tabImports) tabImports.addEventListener('click', () => setComponentActivityTab('imports'));
});
</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}}