Initial commit
This commit is contained in:
270
src/components/Logbook.tsx
Normal file
270
src/components/Logbook.tsx
Normal file
@ -0,0 +1,270 @@
|
||||
/**
|
||||
* @license
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { LogEntry, Device, User } from '../types';
|
||||
import {
|
||||
History, Search, Plus, Hammer, UserIcon, Server,
|
||||
Info, Save, ChevronRight
|
||||
} from 'lucide-react';
|
||||
|
||||
interface LogbookProps {
|
||||
logs: LogEntry[];
|
||||
devices: Device[];
|
||||
users: User[];
|
||||
currentUser: User;
|
||||
onAddLog: (log: Omit<LogEntry, 'id' | 'timestamp'>) => void;
|
||||
}
|
||||
|
||||
export default function Logbook({ logs, devices, users, currentUser, onAddLog }: LogbookProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||
|
||||
// Custom Maintenance Log state
|
||||
const [showAddLog, setShowAddLog] = useState(false);
|
||||
const [targetDeviceId, setTargetDeviceId] = useState('');
|
||||
const [logMessage, setLogMessage] = useState('');
|
||||
|
||||
// Sorted list: latest logs first
|
||||
const sortedLogs = [...logs].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
|
||||
// Filter logs
|
||||
const filteredLogs = sortedLogs.filter(log => {
|
||||
const matchesSearch = log.message.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesType = typeFilter === 'all' || log.type === typeFilter;
|
||||
return matchesSearch && matchesType;
|
||||
});
|
||||
|
||||
const handleSubmitLog = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!logMessage.trim()) return;
|
||||
|
||||
let finalMsg = logMessage;
|
||||
if (targetDeviceId) {
|
||||
const dev = devices.find(x => x.id === targetDeviceId);
|
||||
if (dev) {
|
||||
finalMsg = `[Maintenance on ${dev.hostname}] ${logMessage}`;
|
||||
}
|
||||
}
|
||||
|
||||
onAddLog({
|
||||
type: 'maintenance',
|
||||
message: `${currentUser.name} registered the following maintenance update: ${finalMsg}`,
|
||||
deviceId: targetDeviceId || undefined,
|
||||
userId: currentUser.id
|
||||
});
|
||||
|
||||
setLogMessage('');
|
||||
setTargetDeviceId('');
|
||||
setShowAddLog(false);
|
||||
};
|
||||
|
||||
const getLogTypeBadge = (type: string) => {
|
||||
switch (type) {
|
||||
case 'maintenance':
|
||||
return 'bg-amber-950 border border-amber-900/60 text-amber-400';
|
||||
case 'booking':
|
||||
return 'bg-emerald-950 border border-emerald-900/60 text-emerald-400';
|
||||
case 'status':
|
||||
return 'bg-cyan-950 border border-cyan-900/60 text-cyan-400';
|
||||
case 'system':
|
||||
default:
|
||||
return 'bg-slate-900 border border-slate-800 text-slate-350';
|
||||
}
|
||||
};
|
||||
|
||||
const getLogTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case 'maintenance': return 'Maintenance';
|
||||
case 'booking': return 'Booking';
|
||||
case 'status': return 'Status';
|
||||
case 'system': default: return 'System';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 font-sans" id="logbook-dashboard">
|
||||
|
||||
{/* LEFT COLUMN: Chronological Log List */}
|
||||
<div className="lg:col-span-8 bg-[#1E293B] border border-slate-800 rounded-xl p-5 flex flex-col h-full" id="logbook-ledger-card">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<History className="w-5 h-5 text-emerald-400" />
|
||||
Audit Log & Maintenance Journal
|
||||
</h2>
|
||||
<p className="text-xs text-slate-400">Append-only history of who touched what. git blame, but for the lab.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAddLog(!showAddLog)}
|
||||
className="px-3 py-1.5 bg-slate-900 border border-slate-800 text-slate-200 hover:text-emerald-400 hover:border-emerald-500 rounded-lg text-xs font-semibold flex items-center gap-1.5 transition-all font-sans hover:cursor-pointer"
|
||||
id="btn-toggle-add-log"
|
||||
>
|
||||
<Plus className="w-4 h-4 text-emerald-400" />
|
||||
File Maintenance Report
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters toolbar */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 mb-4 p-3 bg-slate-900/60 border border-slate-850 rounded-lg">
|
||||
<div className="relative flex-1">
|
||||
<span className="absolute left-3 top-2.5 text-slate-550">
|
||||
<Search className="w-4 h-4" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter audit log entries..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full bg-slate-950 text-slate-101 border border-slate-800 rounded-lg pl-9 pr-4 py-1.5 text-xs focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-1 shrink-0 text-xs font-medium">
|
||||
{['all', 'booking', 'maintenance'].map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setTypeFilter(type)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-medium capitalize transition-all ${
|
||||
typeFilter === type
|
||||
? 'bg-emerald-500/15 border border-emerald-500 text-emerald-400'
|
||||
: 'bg-slate-950 text-slate-400 border border-slate-850 hover:bg-slate-850 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{type === 'all' ? 'All' : getLogTypeLabel(type)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Audit Log Sheet */}
|
||||
<div className="flex-1 overflow-y-auto max-h-[550px] space-y-2 pr-1">
|
||||
{filteredLogs.length === 0 ? (
|
||||
<p className="text-center py-16 text-slate-500 text-xs">
|
||||
No audit records match the selected filtering rules.
|
||||
</p>
|
||||
) : (
|
||||
filteredLogs.map((log) => {
|
||||
const dev = devices.find(d => d.id === log.deviceId);
|
||||
const user = users.find(u => u.id === log.userId);
|
||||
const timestampFormatted = new Date(log.timestamp).toLocaleString('en-US', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'medium'
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={log.id} className="p-3 bg-slate-900/40 border border-slate-855 rounded-xl hover:border-slate-800 hover:bg-slate-900/70 transition-all flex items-start gap-3.5">
|
||||
<div className="flex flex-col items-center gap-1.5 shrink-0 pt-0.5 min-w-[80px]">
|
||||
<span className={`text-[9px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded font-sans scale-90 text-center block w-full ${getLogTypeBadge(log.type)}`}>
|
||||
{getLogTypeLabel(log.type)}
|
||||
</span>
|
||||
<span className="text-[9px] font-mono text-slate-500 leading-none">
|
||||
{new Date(log.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-slate-200 leading-relaxed font-sans">{log.message}</p>
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-3 text-[10px] font-mono text-slate-500 pt-1.5 border-t border-slate-900/40">
|
||||
<span>Calendar Time: {timestampFormatted}</span>
|
||||
{user && (
|
||||
<span className="flex items-center gap-1 text-slate-400">
|
||||
<UserIcon className="w-3 h-3 text-slate-500" />
|
||||
Operator: {user.name}
|
||||
</span>
|
||||
)}
|
||||
{dev && (
|
||||
<span className="flex items-center gap-1 text-emerald-450 font-semibold">
|
||||
<Server className="w-3 h-3 text-slate-500" />
|
||||
Node: {dev.hostname}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT COLUMN: Interactive Maintenance Addition Form */}
|
||||
<div className="lg:col-span-4" id="logbook-forms-side">
|
||||
{showAddLog ? (
|
||||
<div className="bg-[#1E293B] border border-slate-800 rounded-xl p-5 shadow-sm space-y-4 animate-in fade-in slide-in-from-right-3 duration-200 font-sans">
|
||||
<div className="flex items-center justify-between pb-2 border-b border-slate-800">
|
||||
<h3 className="font-bold text-sm text-slate-100 flex items-center gap-1.5">
|
||||
<Hammer className="w-4 h-4 text-amber-500" />
|
||||
Journal Maintenance Work
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowAddLog(false)}
|
||||
className="text-slate-400 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmitLog} className="space-y-4 text-xs">
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Target Network Host (Optional)</label>
|
||||
<select
|
||||
value={targetDeviceId}
|
||||
onChange={(e) => setTargetDeviceId(e.target.value)}
|
||||
className="w-full bg-slate-950 text-slate-200 border border-slate-800 rounded p-2 focus:outline-none"
|
||||
>
|
||||
<option value="">-- Complete Lab Cluster / General Event --</option>
|
||||
{devices.map((d) => (
|
||||
<option key={d.id} value={d.id}>
|
||||
{d.hostname} ({d.ip})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Documented Actions / Findings</label>
|
||||
<textarea
|
||||
required
|
||||
rows={4}
|
||||
value={logMessage}
|
||||
onChange={(e) => setLogMessage(e.target.value)}
|
||||
className="w-full bg-slate-950 text-slate-200 border border-slate-800 rounded p-2 focus:outline-none"
|
||||
placeholder="Describe SFP fiber replacements, port trunking alterations, routing table diagnostic evaluations..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900 border border-slate-800 p-2.5 rounded text-[11px] text-slate-450 leading-normal flex gap-2">
|
||||
<Info className="w-4 h-4 text-emerald-400 shrink-0 mt-0.5" />
|
||||
<span>This writes straight to the shared ledger - everyone on the team sees it, so spell it like the next on-call has to read it at 3am.</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-2 bg-emerald-600 hover:bg-emerald-500 text-slate-950 font-bold rounded text-xs flex items-center justify-center gap-1.5 transition-colors"
|
||||
>
|
||||
<Save className="w-3.5 h-3.5" />
|
||||
Publish to Shared Log Book
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-xl p-5 shadow-sm text-xs text-slate-400 font-sans leading-relaxed">
|
||||
<h3 className="font-bold text-white mb-2 text-sm flex items-center gap-2">
|
||||
<ChevronRight className="w-4 h-4 text-emerald-400 shrink-0" />
|
||||
Shared Audit & Fault Logging
|
||||
</h3>
|
||||
<p>
|
||||
Labs drift. If you hit an STP block, a bridging loop or that one port that mysteriously err-disables itself, write down how you dug out. future-you (and the next on-call) will thank you.
|
||||
</p>
|
||||
<div className="mt-4 p-3 bg-amber-950/20 border border-amber-900/40 rounded-lg text-amber-300 font-mono text-[10px]">
|
||||
Tip: hit "File Maintenance Report" up top to log a patch, a swap or a magic-smoke incident.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user