NetworkView: API routing fix, logout button, audit trail, port/notes editor tracking
- Fix frontend API base path (VITE_API_BASE env var, GraphPage hardcoded /api) - Add logout button to NetworkView sidebar (clears portal SSO) - Add AuditTrail component: inline change history on all entity pages - DB migration: add updated_at, last_edited_by to ports table - DB migration: add notes_last_edited_by, notes_updated_at to all entity tables - Backend: track actor on port create/update; notes editor on entity PUT - Frontend: extend types, MarkdownEditor shows last editor, port modal/list show last editor - Fix port CREATE TABLE definition to include new columns upfront - Add try/catch in handleSavePort to surface API errors in modal
This commit is contained in:
@@ -4,6 +4,7 @@ import * as api from '../api';
|
||||
import type { Component, Port } from '../types';
|
||||
import { COMPONENT_META, COMPONENT_TYPES } from '../types';
|
||||
import MarkdownEditor from '../components/MarkdownEditor';
|
||||
import AuditTrail from '../components/AuditTrail';
|
||||
|
||||
const STATUS_OPTIONS = ['active', 'maintenance', 'decommissioned'] as const;
|
||||
|
||||
@@ -141,21 +142,26 @@ export default function ComponentPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (portModal?.editPort) {
|
||||
await api.updatePort(componentId, portModal.editPort.id, {
|
||||
label: portForm.label || undefined,
|
||||
port_type: portForm.port_type,
|
||||
notes: portForm.notes || undefined,
|
||||
connected_to_port_id: connectedToPortId,
|
||||
});
|
||||
} else {
|
||||
await api.createPort(componentId, {
|
||||
port_number: Number(portForm.port_number),
|
||||
label: portForm.label || undefined,
|
||||
port_type: portForm.port_type,
|
||||
notes: portForm.notes || undefined,
|
||||
connected_to_port_id: connectedToPortId ?? undefined,
|
||||
});
|
||||
try {
|
||||
if (portModal?.editPort) {
|
||||
await api.updatePort(componentId, portModal.editPort.id, {
|
||||
label: portForm.label || undefined,
|
||||
port_type: portForm.port_type,
|
||||
notes: portForm.notes || undefined,
|
||||
connected_to_port_id: connectedToPortId,
|
||||
});
|
||||
} else {
|
||||
await api.createPort(componentId, {
|
||||
port_number: Number(portForm.port_number),
|
||||
label: portForm.label || undefined,
|
||||
port_type: portForm.port_type,
|
||||
notes: portForm.notes || undefined,
|
||||
connected_to_port_id: connectedToPortId ?? undefined,
|
||||
});
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
setPortError(err instanceof Error ? err.message : 'Failed to save port');
|
||||
return;
|
||||
}
|
||||
setPortModal(null);
|
||||
setSwLink(null);
|
||||
@@ -353,6 +359,14 @@ export default function ComponentPage() {
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
{port.last_edited_by && (
|
||||
<div className="port-edited-by">
|
||||
✎ {port.last_edited_by}
|
||||
{port.updated_at && (
|
||||
<> · {new Date(port.updated_at).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button className="port-edit-btn" onClick={() => openPortModal(port)} title="Edit port">✎</button>
|
||||
<button className="port-delete" onClick={() => handleDeletePort(port.id)} title="Delete port">✕</button>
|
||||
@@ -372,6 +386,14 @@ export default function ComponentPage() {
|
||||
? `Edit ${component.type === 'patch_panel' ? 'PP' : 'SW'} Port ${portModal.editPort.port_number}`
|
||||
: `Add ${component.type === 'patch_panel' ? 'PP Port' : 'SW Port'}`}
|
||||
</span>
|
||||
{portModal.editPort?.last_edited_by && (
|
||||
<span className="modal-last-edited">
|
||||
✎ {portModal.editPort.last_edited_by}
|
||||
{portModal.editPort.updated_at && (
|
||||
<> · {new Date(portModal.editPort.updated_at).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}</>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<button className="modal-close" onClick={closePortModal}>✕</button>
|
||||
</div>
|
||||
<form onSubmit={handleSavePort} style={{ padding: '20px', display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
@@ -464,8 +486,16 @@ export default function ComponentPage() {
|
||||
{/* Notes */}
|
||||
<section className="content-section">
|
||||
<h2 className="section-title">Documentation</h2>
|
||||
<MarkdownEditor value={notes} onChange={setNotes} onSave={saveNotes} />
|
||||
<MarkdownEditor
|
||||
value={notes}
|
||||
onChange={setNotes}
|
||||
onSave={saveNotes}
|
||||
lastEditedBy={component.notes_last_edited_by}
|
||||
lastEditedAt={component.notes_updated_at}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<AuditTrail entityId={component.id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user