Files
ld-sysinfo-frontend/src/components/DeviceListSidebar.tsx
2025-09-19 03:26:52 +00:00

246 lines
6.4 KiB
TypeScript

// src/components/DeviceListSidebar.tsx
'use client';
import React from 'react';
import { List, ListItemButton, ListItemIcon, ListItemText, Typography, Box,TextField } from '@mui/material';
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
import { parseISO, formatDistanceToNowStrict } from 'date-fns';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import { DetailedDevice } from '@/types/devices';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
interface DeviceListSidebarProps {
devices: DetailedDevice[];
selectedDevice: number | null;
onSelect: (deviceId: number) => void;
open: boolean;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
pinned?: boolean;
onTogglePin?: () => void;
}
const ONLINE_THRESHOLD_MINUTES = 1440;
function getStatusColor(lastCheckedIn: string) {
const lastSeen = parseISO(lastCheckedIn);
const minutesAgo = (Date.now() - lastSeen.getTime()) / 1000 / 60;
return minutesAgo < ONLINE_THRESHOLD_MINUTES ? 'green' : 'grey';
}
function formatRelativeTime(lastCheckedIn: string) {
try {
return formatDistanceToNowStrict(parseISO(lastCheckedIn), { addSuffix: true });
} catch {
return 'Unknown';
}
}
const DeviceListSidebar: React.FC<DeviceListSidebarProps> = ({
devices,
selectedDevice,
onSelect,
open,
onMouseEnter,
onMouseLeave,
pinned,
onTogglePin,
}) => {
const [sortAsc, setSortAsc] = React.useState(true);
const [searchQuery, setSearchQuery] = React.useState('');
return (
<Box
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
sx={{
position: 'relative',
height: '100vh',
width: open ? 200 : 200,
minWidth: open ? 200 : 200,
transition: 'width 0.3s ease',
flexShrink: 0,
borderRight: '1px solid #ccc',
bgcolor: 'background.paper',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
}}
>
<Box sx={{ px: 2, py: 1 }}>
<TextField
size="small"
fullWidth
placeholder="Search devices..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
variant="outlined"
sx={{ mt: 1 }}
/>
</Box>
{devices.length === 0 && (
<Typography sx={{ px: 2, py: 1, color: 'error.main' }}>
No devices received (length: 0)
</Typography>
)}
{/* Header */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start', // 👈 always left-aligned
px: 2,
py: 1.5,
borderBottom: '1px solid',
borderColor: 'divider',
gap: 1,
}}
>
<Typography
variant="subtitle1"
noWrap
sx={{
fontWeight: 600,
opacity: open ? 1 : 1,
transition: 'opacity 0.2s ease',
}}
>
Select a Device
</Typography>
<IconButton
size="small"
onClick={() => setSortAsc((prev) => !prev)}
sx={{ ml: 'auto' }} // Pushes it to the right
>
<Tooltip title={`Sort by Hostname (${sortAsc ? 'A-Z' : 'Z-A'})`}>
{sortAsc ? <ArrowUpwardIcon fontSize="small" /> : <ArrowDownwardIcon fontSize="small" />}
</Tooltip>
</IconButton>
</Box>
{open && onTogglePin && (
<Tooltip title={pinned ? 'Unpin Sidebar' : 'Pin Sidebar'}>
<IconButton
size="medium"
onClick={(e) => {
e.stopPropagation();
onTogglePin?.();
}}
sx={(theme) => ({
position: 'absolute',
right: -10,
top: 'calc(50% - 64px)', // offsets for AppBar height
transform: 'translateY(-50%)',
backgroundColor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
borderRadius: '0 4px 4px 0',
zIndex: theme.zIndex.drawer + 1,
transition: 'background-color 0.2s ease-in-out',
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
})}
>
{pinned ? <ChevronLeftIcon /> : <ChevronRightIcon />}
</IconButton>
</Tooltip>
)}
{/* Device list */}
<List dense={!open}>
{[...devices]
.sort((a, b) =>
sortAsc
? a.hostname.localeCompare(b.hostname)
: b.hostname.localeCompare(a.hostname)
)
.filter((device) =>
device.hostname.toLowerCase().includes(searchQuery.toLowerCase())
)
.map((device) => (
<ListItemButton
key={device.deviceId}
selected={device.deviceId === selectedDevice}
onClick={() => onSelect(device.deviceId)}
sx={{
alignItems: 'flex-start',
minHeight: 56,
px: open ? 2 : 1.5,
py: 1,
}}
>
<ListItemIcon
sx={{
minWidth: 0,
mr: open ? 1.5 : 1,
mt: 0.5,
justifyContent: 'center',
}}
>
<FiberManualRecordIcon sx={{ color: getStatusColor(device.lastCheckedIn), fontSize: 12 }} />
</ListItemIcon>
<Box
sx={{
flexGrow: 1,
minWidth: 0, // 🚨 ensures text truncation works
}}
>
{/* Hostname: Always visible */}
<Typography
noWrap
title={device.hostname}
sx={{
fontSize: '0.875rem',
fontWeight: 500,
opacity: 1,
}}
>
{device.hostname}
</Typography>
{/* Last seen: Only visible when expanded */}
<Typography
variant="caption"
sx={{
opacity: open ? 1 : 0,
maxHeight: open ? 16 : 0,
overflow: 'hidden',
whiteSpace: 'nowrap',
transition: 'opacity 0.2s ease, max-height 0.2s ease',
}}
>
🕒 {formatRelativeTime(device.lastCheckedIn)}
</Typography>
</Box>
</ListItemButton>
))}
</List>
</Box>
);
};
export default DeviceListSidebar;