284 lines
12 KiB
TypeScript
284 lines
12 KiB
TypeScript
/**
|
|
* @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');
|
|
const [showSystem, setShowSystem] = useState(false);
|
|
|
|
// 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 => {
|
|
if (!showSystem && log.type === 'system') return false;
|
|
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 flex-wrap">
|
|
{['all', 'booking', 'maintenance', 'status'].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>
|
|
))}
|
|
<button
|
|
onClick={() => setShowSystem(v => !v)}
|
|
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
|
showSystem
|
|
? 'bg-slate-700/40 border border-slate-600 text-slate-300'
|
|
: 'bg-slate-950 text-slate-600 border border-slate-850 hover:text-slate-400'
|
|
}`}
|
|
title="Show/hide automated system & CheckMK entries"
|
|
>
|
|
System
|
|
</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>
|
|
);
|
|
}
|