Remove UI for machine details
This commit is contained in:
@@ -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/>},
|
||||||
|
@@ -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;
|
|
@@ -1,2 +0,0 @@
|
|||||||
export { default } from './MachineDetails.jsx';
|
|
||||||
export { MachineDetails } from './MachineDetails.jsx';
|
|
@@ -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%)
|
|
@@ -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"
|
||||||
|
Reference in New Issue
Block a user