Remove UI for machine details

This commit is contained in:
Mathias Wagner
2025-09-10 11:11:53 +02:00
parent e595fcbdac
commit 0a16e46372
5 changed files with 2 additions and 670 deletions

View File

@@ -6,7 +6,6 @@ import Root from "@/common/layouts/Root.jsx";
import UserManagement from "@/pages/UserManagement"; import UserManagement from "@/pages/UserManagement";
import SystemSettings from "@/pages/SystemSettings"; import SystemSettings from "@/pages/SystemSettings";
import Machines from "@/pages/Machines"; import Machines from "@/pages/Machines";
import MachineDetails from "@/pages/MachineDetails";
import "@fontsource/plus-jakarta-sans/300.css"; import "@fontsource/plus-jakarta-sans/300.css";
import "@fontsource/plus-jakarta-sans/400.css"; import "@fontsource/plus-jakarta-sans/400.css";
import "@fontsource/plus-jakarta-sans/600.css"; import "@fontsource/plus-jakarta-sans/600.css";
@@ -25,7 +24,6 @@ const App = () => {
{path: "/", element: <Navigate to="/dashboard"/>}, {path: "/", element: <Navigate to="/dashboard"/>},
{path: "/dashboard", element: <Placeholder title="Dashboard"/>}, {path: "/dashboard", element: <Placeholder title="Dashboard"/>},
{path: "/machines", element: <Machines/>}, {path: "/machines", element: <Machines/>},
{path: "/machines/:id", element: <MachineDetails/>},
{path: "/servers", element: <Placeholder title="Servers"/>}, {path: "/servers", element: <Placeholder title="Servers"/>},
{path: "/settings", element: <Placeholder title="Settings"/>}, {path: "/settings", element: <Placeholder title="Settings"/>},
{path: "/admin/users", element: <UserManagement/>}, {path: "/admin/users", element: <UserManagement/>},

View File

@@ -1,416 +0,0 @@
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,
Eye,
ArrowCircleLeft
} 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);
const [snapshotDetails, setSnapshotDetails] = useState(null);
const [loadingDetails, setLoadingDetails] = useState(false);
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 fetchSnapshotDetails = async (snapshotId) => {
try {
setLoadingDetails(true);
const details = await getRequest(`machines/${id}/snapshots/${snapshotId}`);
setSnapshotDetails(details);
setSelectedSnapshot(snapshotId);
} catch (error) {
console.error('Failed to fetch snapshot details:', error);
toast.error('Failed to load snapshot details');
} finally {
setLoadingDetails(false);
}
};
const backToSnapshots = () => {
setSelectedSnapshot(null);
setSnapshotDetails(null);
};
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 {
// Handle both "2025-09-09 20:19:48" and "2025-09-09 20:19:48 UTC" formats
const cleanDate = dateString.replace(' UTC', '');
const date = new Date(cleanDate);
if (isNaN(date.getTime())) {
return dateString; // Return original if parsing fails
}
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} catch {
return dateString;
}
};
const formatLBA = (lba) => {
if (!lba && lba !== 0) return '0';
return lba.toLocaleString();
};
const getFsTypeColor = (fsType) => {
switch (fsType?.toLowerCase()) {
case 'ext':
case 'ext4':
case 'ext3':
case 'ext2':
return 'success';
case 'ntfs':
return 'info';
case 'fat32':
case 'fat':
return 'warning';
case 'xfs':
return 'info';
case 'btrfs':
return 'success';
default:
return 'secondary';
}
};
const truncateHash = (hash, length = 16) => {
if (!hash) return 'Unknown';
return hash.length > length ? `${hash.substring(0, length)}...` : hash;
};
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={
selectedSnapshot
? `Snapshot Details`
: `Machine ID: ${machine.machine_id}`
}
actions={
selectedSnapshot ? (
<Button variant="secondary" onClick={backToSnapshots}>
<ArrowCircleLeft size={16} />
Back to Snapshots
</Button>
) : (
<Button variant="secondary" onClick={() => navigate('/machines')}>
<ArrowLeft size={16} />
Back to Machines
</Button>
)
}
/>
<Grid columns={1} gap="large">
{/* Machine Information - Only show when not viewing snapshot details */}
{!selectedSnapshot && (
<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 List or Details */}
{!selectedSnapshot ? (
/* Snapshots List */
<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-summary-card">
<CardBody>
<div className="snapshot-summary">
<div className="snapshot-info">
<div className="snapshot-title">
<Camera size={18} />
<h4>Snapshot</h4>
</div>
<DetailList>
<DetailItem
label="Created"
value={
<div className="snapshot-date">
<Calendar size={14} />
{formatDate(snapshot.created_at)}
</div>
}
/>
<DetailItem
label="Snapshot ID"
value={
<div className="snapshot-hash">
<Hash size={14} />
<code>{truncateHash(snapshot.id, 24)}</code>
</div>
}
/>
<DetailItem
label="Hash"
value={
<div className="snapshot-hash">
<Hash size={14} />
<code>{truncateHash(snapshot.snapshot_hash, 24)}</code>
</div>
}
/>
</DetailList>
</div>
<div className="snapshot-actions">
<Button
variant="primary"
size="small"
onClick={() => fetchSnapshotDetails(snapshot.id)}
>
<Eye size={16} />
View Details
</Button>
</div>
</div>
</CardBody>
</Card>
))}
</Grid>
)}
</CardBody>
</Card>
) : (
/* Snapshot Details */
<Card>
<CardHeader>
<h3><Camera size={20} /> Snapshot {selectedSnapshot} Details</h3>
</CardHeader>
<CardBody>
{loadingDetails ? (
<LoadingSpinner />
) : snapshotDetails ? (
<div className="snapshot-details">
{/* Snapshot Metadata */}
<Card className="snapshot-metadata">
<CardHeader>
<h4><Database size={18} /> Metadata</h4>
</CardHeader>
<CardBody>
<DetailList>
<DetailItem
label="Created"
value={
<div className="snapshot-date">
<Calendar size={14} />
{formatDate(snapshotDetails.created_at)}
</div>
}
/>
<DetailItem
label="Hash"
value={
<div className="snapshot-hash">
<Hash size={14} />
<code>{snapshotDetails.snapshot_hash}</code>
</div>
}
/>
<DetailItem
label="Disks"
value={`${snapshotDetails.disks.length} disk${snapshotDetails.disks.length !== 1 ? 's' : ''}`}
/>
</DetailList>
</CardBody>
</Card>
{/* Disks */}
<div className="disks-section">
<h4><HardDrive size={18} /> Disks ({snapshotDetails.disks.length})</h4>
<Grid columns={1} gap="medium">
{snapshotDetails.disks.map((disk, diskIndex) => (
<Card key={diskIndex} className="disk-card">
<CardHeader>
<h5><HardDrive size={16} /> Disk {diskIndex + 1}</h5>
</CardHeader>
<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="1rem" minWidth="280px">
{disk.partitions.map((partition, partIndex) => (
<Card key={partIndex} className="partition-card">
<CardHeader>
<div className="partition-header">
<span>Partition {partIndex + 1}</span>
<Badge variant={getFsTypeColor(partition.fs_type)}>
{partition.fs_type.toUpperCase()}
</Badge>
</div>
</CardHeader>
<CardBody>
<DetailList>
<DetailItem label="Size" value={formatBytes(partition.size_bytes)} />
<DetailItem label="Start LBA" value={formatLBA(partition.start_lba)} />
<DetailItem label="End LBA" value={formatLBA(partition.end_lba)} />
<DetailItem
label="Sectors"
value={formatLBA(partition.end_lba - partition.start_lba)}
/>
</DetailList>
</CardBody>
</Card>
))}
</Grid>
</div>
)}
</CardBody>
</Card>
))}
</Grid>
</div>
</div>
) : (
<EmptyState
icon={<Camera size={48} weight="duotone" />}
title="No Details Available"
subtitle="Unable to load snapshot details."
/>
)}
</CardBody>
</Card>
)}
</Grid>
</div>
);
};
export default MachineDetails;

View File

@@ -1,2 +0,0 @@
export { default } from './MachineDetails.jsx';
export { MachineDetails } from './MachineDetails.jsx';

View File

@@ -1,232 +0,0 @@
// Machine Details Page Styles
.machine-details
// Snapshot Summary Cards (list view)
.snapshot-summary-card
transition: all 0.2s ease
cursor: pointer
&:hover
border-color: var(--border-strong)
box-shadow: 0 4px 12px rgba(31, 36, 41, 0.1)
transform: translateY(-1px)
.snapshot-summary
display: flex
justify-content: space-between
align-items: flex-start
gap: 1.5rem
.snapshot-info
flex: 1
.snapshot-title
display: flex
align-items: center
gap: 0.75rem
margin-bottom: 1rem
h4
font-size: 1.125rem
font-weight: 600
color: var(--text)
margin: 0
.snapshot-date
display: flex
align-items: center
gap: 0.5rem
font-size: 0.875rem
color: var(--text-dim)
.snapshot-hash
display: flex
align-items: center
gap: 0.5rem
font-size: 0.875rem
code
background: var(--bg-elev)
padding: 0.25rem 0.5rem
border-radius: var(--radius-sm)
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace
color: var(--text-dim)
font-size: 0.8rem
.snapshot-actions
display: flex
flex-direction: column
gap: 0.5rem
// Snapshot Detail View
.snapshot-details
.snapshot-metadata
margin-bottom: 2rem
background: linear-gradient(135deg, var(--bg-alt) 0%, var(--bg-elev) 100%)
border: 1px solid var(--border)
.disks-section
h4
font-size: 1.25rem
font-weight: 600
color: var(--text)
margin-bottom: 1.5rem
display: flex
align-items: center
gap: 0.75rem
padding-bottom: 0.5rem
border-bottom: 2px solid var(--border)
.disk-card
border: 1px solid var(--border)
background: linear-gradient(135deg, var(--bg-alt) 0%, var(--bg-elev) 100%)
transition: all 0.2s ease
position: relative
overflow: hidden
&::before
content: ''
position: absolute
top: 0
left: 0
right: 0
height: 3px
background: linear-gradient(90deg, var(--accent) 0%, var(--success) 100%)
opacity: 0
transition: opacity 0.2s ease
&:hover
border-color: var(--border-strong)
box-shadow: 0 6px 20px rgba(31, 36, 41, 0.15)
transform: translateY(-2px)
&::before
opacity: 1
.partitions-section
margin-top: 2rem
h6
font-size: 1rem
font-weight: 600
color: var(--text)
margin-bottom: 1rem
display: flex
align-items: center
gap: 0.5rem
padding: 0.5rem 0
border-bottom: 1px solid var(--border)
.partition-card
border: 1px solid var(--border)
background: var(--bg-elev)
transition: all 0.2s ease
position: relative
&:hover
border-color: var(--border-strong)
box-shadow: 0 3px 10px rgba(31, 36, 41, 0.1)
transform: translateY(-1px)
.partition-header
display: flex
justify-content: space-between
align-items: center
span
font-size: 0.875rem
font-weight: 600
color: var(--text)
// Enhanced visual feedback
.snapshot-date, .snapshot-hash
transition: color 0.2s ease
&:hover
color: var(--text)
// Better spacing for detail items
.detail-list
.detail-item
padding: 0.75rem 0
border-bottom: 1px solid var(--border)
&:last-child
border-bottom: none
.detail-label
font-weight: 500
color: var(--text-dim)
font-size: 0.875rem
text-transform: uppercase
letter-spacing: 0.05em
.detail-value
font-weight: 500
color: var(--text)
code
background: var(--bg-elev)
padding: 0.25rem 0.5rem
border-radius: var(--radius-sm)
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace
font-size: 0.8rem
border: 1px solid var(--border)
// Loading and error states
.loading-section
text-align: center
padding: 3rem
.spinner
border: 3px solid var(--border)
border-top: 3px solid var(--accent)
border-radius: 50%
width: 40px
height: 40px
animation: spin 1s linear infinite
margin: 0 auto 1rem
@keyframes spin
0%
transform: rotate(0deg)
100%
transform: rotate(360deg)
// Responsive design
@media (max-width: 768px)
.snapshot-summary
flex-direction: column
gap: 1rem
.snapshot-actions
flex-direction: row
align-self: stretch
.disk-card .partitions-section h6
font-size: 0.875rem
.disks-section h4
font-size: 1.125rem
// Visual hierarchy improvements
.card
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1)
&:hover
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15)
.badge
font-weight: 600
letter-spacing: 0.025em
&.variant-success
background: linear-gradient(135deg, var(--success) 0%, #22c55e 100%)
&.variant-info
background: linear-gradient(135deg, var(--info) 0%, #3b82f6 100%)
&.variant-warning
background: linear-gradient(135deg, var(--warning) 0%, #f59e0b 100%)
&.variant-secondary
background: linear-gradient(135deg, var(--text-dim) 0%, #6b7280 100%)

View File

@@ -1,5 +1,4 @@
import React, {useState, useEffect, useContext} from 'react'; import React, {useState, useEffect, useContext} from 'react';
import {useNavigate} from 'react-router-dom';
import {UserContext} from '@/common/contexts/UserContext.jsx'; import {UserContext} from '@/common/contexts/UserContext.jsx';
import {useToast} from '@/common/contexts/ToastContext.jsx'; import {useToast} from '@/common/contexts/ToastContext.jsx';
import {getRequest, postRequest, deleteRequest} from '@/common/utils/RequestUtil.js'; import {getRequest, postRequest, deleteRequest} from '@/common/utils/RequestUtil.js';
@@ -29,7 +28,6 @@ import './styles.sass';
export const Machines = () => { export const Machines = () => {
const {user: currentUser} = useContext(UserContext); const {user: currentUser} = useContext(UserContext);
const toast = useToast(); const toast = useToast();
const navigate = useNavigate();
const [machines, setMachines] = useState([]); const [machines, setMachines] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
@@ -181,14 +179,6 @@ export const Machines = () => {
} }
}; };
const handleMachineClick = (machineId) => {
navigate(`/machines/${machineId}`);
};
const handleActionClick = (e) => {
e.stopPropagation(); // Prevent navigation when clicking action buttons
};
const handleInputChange = (e) => { const handleInputChange = (e) => {
const {name, value} = e.target; const {name, value} = e.target;
setFormData(prev => ({ setFormData(prev => ({
@@ -230,13 +220,7 @@ export const Machines = () => {
<Grid minWidth="400px"> <Grid minWidth="400px">
{machines.map(machine => ( {machines.map(machine => (
<Card <Card key={machine.id} hover className="machine-card">
key={machine.id}
hover
className="machine-card"
onClick={() => handleMachineClick(machine.id)}
style={{ cursor: 'pointer' }}
>
<CardHeader> <CardHeader>
<div className="machine-card-header"> <div className="machine-card-header">
<div className="machine-icon"> <div className="machine-icon">
@@ -249,7 +233,7 @@ export const Machines = () => {
<span className="uuid-text">{formatUuid(machine.uuid)}</span> <span className="uuid-text">{formatUuid(machine.uuid)}</span>
</div> </div>
</div> </div>
<div className="machine-actions" onClick={handleActionClick}> <div className="machine-actions">
<Button <Button
variant="subtle" variant="subtle"
size="sm" size="sm"