267 lines
13 KiB
JavaScript
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;
|