Files
Arkendro/webui/src/pages/MachineDetails/MachineDetails.jsx
2025-09-09 22:23:01 +02:00

267 lines
13 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { getRequest } from '@/common/utils/RequestUtil.js';
import { useToast } from '@/common/contexts/ToastContext.jsx';
import Card, { CardHeader, CardBody } from '@/common/components/Card';
import Grid from '@/common/components/Grid';
import LoadingSpinner from '@/common/components/LoadingSpinner';
import EmptyState from '@/common/components/EmptyState';
import PageHeader from '@/common/components/PageHeader';
import DetailItem, { DetailList } from '@/common/components/DetailItem';
import Badge from '@/common/components/Badge';
import Button from '@/common/components/Button';
import {
ArrowLeft,
Camera,
HardDrive,
Folder,
Calendar,
Hash,
Database,
Devices
} from '@phosphor-icons/react';
import './styles.sass';
export const MachineDetails = () => {
const { id } = useParams();
const navigate = useNavigate();
const toast = useToast();
const [machine, setMachine] = useState(null);
const [snapshots, setSnapshots] = useState([]);
const [loading, setLoading] = useState(true);
const [selectedSnapshot, setSelectedSnapshot] = useState(null);
useEffect(() => {
if (id) {
fetchMachineData();
}
}, [id]);
const fetchMachineData = async () => {
try {
setLoading(true);
// Fetch machine info and snapshots in parallel
const [machineResponse, snapshotsResponse] = await Promise.all([
getRequest(`machines/${id}`),
getRequest(`machines/${id}/snapshots`)
]);
setMachine(machineResponse);
setSnapshots(snapshotsResponse);
} catch (error) {
console.error('Failed to fetch machine data:', error);
toast.error('Failed to load machine details');
} finally {
setLoading(false);
}
};
const formatBytes = (bytes) => {
if (!bytes) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
};
const formatDate = (dateString) => {
if (!dateString || dateString === 'Unknown') return 'Unknown';
try {
return new Date(dateString).toLocaleString();
} catch {
return dateString;
}
};
const getFsTypeColor = (fsType) => {
switch (fsType?.toLowerCase()) {
case 'ext': return 'success';
case 'ntfs': return 'info';
case 'fat32': return 'warning';
default: return 'secondary';
}
};
if (loading) {
return (
<div className="machine-details">
<PageHeader
title="Loading..."
subtitle="Fetching machine details"
actions={
<Button variant="secondary" onClick={() => navigate('/machines')}>
<ArrowLeft size={16} />
Back to Machines
</Button>
}
/>
<LoadingSpinner />
</div>
);
}
if (!machine) {
return (
<div className="machine-details">
<PageHeader
title="Machine Not Found"
subtitle="The requested machine could not be found"
actions={
<Button variant="secondary" onClick={() => navigate('/machines')}>
<ArrowLeft size={16} />
Back to Machines
</Button>
}
/>
<EmptyState
icon={<Devices size={48} weight="duotone" />}
title="Machine Not Found"
subtitle="This machine may have been deleted or you don't have access to it."
/>
</div>
);
}
return (
<div className="machine-details">
<PageHeader
title={machine.name}
subtitle={`Machine ID: ${machine.machine_id}`}
actions={
<Button variant="secondary" onClick={() => navigate('/machines')}>
<ArrowLeft size={16} />
Back to Machines
</Button>
}
/>
<Grid columns={1} gap="large">
{/* Machine Information */}
<Card>
<CardHeader>
<h3><Devices size={20} /> Machine Information</h3>
</CardHeader>
<CardBody>
<DetailList>
<DetailItem label="Name" value={machine.name} />
<DetailItem label="Machine ID" value={machine.machine_id} />
<DetailItem label="Created" value={formatDate(machine.created_at)} />
<DetailItem label="Status" value={
<Badge variant="success">Active</Badge>
} />
</DetailList>
</CardBody>
</Card>
{/* Snapshots */}
<Card>
<CardHeader>
<h3><Camera size={20} /> Snapshots ({snapshots.length})</h3>
</CardHeader>
<CardBody>
{snapshots.length === 0 ? (
<EmptyState
icon={<Camera size={48} weight="duotone" />}
title="No Snapshots"
subtitle="This machine hasn't created any snapshots yet."
/>
) : (
<Grid columns={1} gap="medium">
{snapshots.map((snapshot) => (
<Card key={snapshot.id} className="snapshot-card">
<CardHeader>
<div className="snapshot-header">
<h4>
<Camera size={16} />
Snapshot #{snapshot.id}
</h4>
<Badge variant="secondary">
{snapshot.disks.length} disk{snapshot.disks.length !== 1 ? 's' : ''}
</Badge>
</div>
</CardHeader>
<CardBody>
<DetailList>
<DetailItem
label="Created"
value={
<div className="snapshot-date">
<Calendar size={14} />
{formatDate(snapshot.created_at)}
</div>
}
/>
<DetailItem
label="Hash"
value={
<div className="snapshot-hash">
<Hash size={14} />
<code>{snapshot.snapshot_hash.substring(0, 16)}...</code>
</div>
}
/>
</DetailList>
{/* Disks */}
<div className="disks-section">
<h5><HardDrive size={16} /> Disks</h5>
<Grid columns={1} gap="small">
{snapshot.disks.map((disk, diskIndex) => (
<Card key={diskIndex} className="disk-card">
<CardBody>
<DetailList>
<DetailItem label="Serial" value={disk.serial || 'Unknown'} />
<DetailItem label="Size" value={formatBytes(disk.size_bytes)} />
<DetailItem
label="Partitions"
value={`${disk.partitions.length} partition${disk.partitions.length !== 1 ? 's' : ''}`}
/>
</DetailList>
{/* Partitions */}
{disk.partitions.length > 0 && (
<div className="partitions-section">
<h6><Folder size={14} /> Partitions</h6>
<Grid columns="auto-fit" gap="small" minColumnWidth="250px">
{disk.partitions.map((partition, partIndex) => (
<Card key={partIndex} className="partition-card">
<CardBody>
<DetailList>
<DetailItem
label="Filesystem"
value={
<Badge variant={getFsTypeColor(partition.fs_type)}>
{partition.fs_type.toUpperCase()}
</Badge>
}
/>
<DetailItem label="Size" value={formatBytes(partition.size_bytes)} />
<DetailItem label="Start LBA" value={partition.start_lba.toLocaleString()} />
<DetailItem label="End LBA" value={partition.end_lba.toLocaleString()} />
</DetailList>
</CardBody>
</Card>
))}
</Grid>
</div>
)}
</CardBody>
</Card>
))}
</Grid>
</div>
</CardBody>
</Card>
))}
</Grid>
)}
</CardBody>
</Card>
</Grid>
</div>
);
};
export default MachineDetails;