Add working test
This commit is contained in:
266
webui/src/pages/MachineDetails/MachineDetails.jsx
Normal file
266
webui/src/pages/MachineDetails/MachineDetails.jsx
Normal file
@@ -0,0 +1,266 @@
|
||||
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;
|
Reference in New Issue
Block a user