Add working test
This commit is contained in:
@@ -18,7 +18,9 @@ import {
|
||||
Calendar,
|
||||
Hash,
|
||||
Database,
|
||||
Devices
|
||||
Devices,
|
||||
Eye,
|
||||
ArrowCircleLeft
|
||||
} from '@phosphor-icons/react';
|
||||
import './styles.sass';
|
||||
|
||||
@@ -30,6 +32,8 @@ export const MachineDetails = () => {
|
||||
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) {
|
||||
@@ -57,6 +61,25 @@ export const MachineDetails = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -68,21 +91,56 @@ export const MachineDetails = () => {
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString || dateString === 'Unknown') return 'Unknown';
|
||||
try {
|
||||
return new Date(dateString).toLocaleString();
|
||||
// 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': return 'success';
|
||||
case 'ntfs': return 'info';
|
||||
case 'fat32': return 'warning';
|
||||
default: return 'secondary';
|
||||
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">
|
||||
@@ -127,59 +185,134 @@ export const MachineDetails = () => {
|
||||
<div className="machine-details">
|
||||
<PageHeader
|
||||
title={machine.name}
|
||||
subtitle={`Machine ID: ${machine.machine_id}`}
|
||||
subtitle={
|
||||
selectedSnapshot
|
||||
? `Snapshot Details`
|
||||
: `Machine ID: ${machine.machine_id}`
|
||||
}
|
||||
actions={
|
||||
<Button variant="secondary" onClick={() => navigate('/machines')}>
|
||||
<ArrowLeft size={16} />
|
||||
Back to Machines
|
||||
</Button>
|
||||
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 */}
|
||||
<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>
|
||||
{/* 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 */}
|
||||
<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">
|
||||
{/* 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>
|
||||
<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>
|
||||
<h4><Database size={18} /> Metadata</h4>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<DetailList>
|
||||
@@ -188,7 +321,7 @@ export const MachineDetails = () => {
|
||||
value={
|
||||
<div className="snapshot-date">
|
||||
<Calendar size={14} />
|
||||
{formatDate(snapshot.created_at)}
|
||||
{formatDate(snapshotDetails.created_at)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
@@ -197,67 +330,84 @@ export const MachineDetails = () => {
|
||||
value={
|
||||
<div className="snapshot-hash">
|
||||
<Hash size={14} />
|
||||
<code>{snapshot.snapshot_hash.substring(0, 16)}...</code>
|
||||
<code>{snapshotDetails.snapshot_hash}</code>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<DetailItem
|
||||
label="Disks"
|
||||
value={`${snapshotDetails.disks.length} disk${snapshotDetails.disks.length !== 1 ? 's' : ''}`}
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
|
@@ -1,250 +1,232 @@
|
||||
// Variables are defined in main.sass root scope
|
||||
|
||||
// Machine Details Page Styles
|
||||
.machine-details
|
||||
.machine-header
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1rem
|
||||
margin-bottom: 2rem
|
||||
|
||||
.back-button
|
||||
padding: 0.5rem
|
||||
border-radius: var(--radius)
|
||||
border: 1px solid var(--border)
|
||||
background: var(--bg-alt)
|
||||
color: var(--text)
|
||||
cursor: pointer
|
||||
transition: all 0.2s ease
|
||||
|
||||
&:hover
|
||||
background: var(--bg-elev)
|
||||
border-color: var(--border-strong)
|
||||
|
||||
.machine-title
|
||||
flex: 1
|
||||
|
||||
h1
|
||||
font-size: 1.5rem
|
||||
font-weight: 600
|
||||
color: var(--text)
|
||||
margin-bottom: 0.25rem
|
||||
|
||||
.machine-uuid
|
||||
font-family: 'Courier New', monospace
|
||||
font-size: 0.875rem
|
||||
color: var(--text-dim)
|
||||
background: var(--bg-elev)
|
||||
padding: 0.25rem 0.5rem
|
||||
border-radius: var(--radius-sm)
|
||||
display: inline-block
|
||||
|
||||
.snapshots-section
|
||||
h2
|
||||
font-size: 1.25rem
|
||||
font-weight: 600
|
||||
color: var(--text)
|
||||
margin-bottom: 1rem
|
||||
|
||||
.snapshots-grid
|
||||
display: grid
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr))
|
||||
gap: 1.5rem
|
||||
|
||||
.snapshot-card
|
||||
border: 1px solid var(--border)
|
||||
border-radius: var(--radius-lg)
|
||||
background: var(--bg-alt)
|
||||
padding: 1.5rem
|
||||
transition: all 0.2s ease
|
||||
|
||||
&:hover
|
||||
border-color: var(--border-strong)
|
||||
box-shadow: 0 2px 8px rgba(31, 36, 41, 0.1)
|
||||
|
||||
.snapshot-header
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: flex-start
|
||||
margin-bottom: 1rem
|
||||
// Snapshot Summary Cards (list view)
|
||||
.snapshot-summary-card
|
||||
transition: all 0.2s ease
|
||||
cursor: pointer
|
||||
|
||||
.snapshot-info
|
||||
h3
|
||||
font-size: 1rem
|
||||
font-weight: 600
|
||||
color: var(--text)
|
||||
margin-bottom: 0.25rem
|
||||
|
||||
.snapshot-hash
|
||||
font-family: 'Courier New', monospace
|
||||
font-size: 0.75rem
|
||||
color: var(--text-dim)
|
||||
background: var(--bg-elev)
|
||||
padding: 0.125rem 0.375rem
|
||||
border-radius: var(--radius-sm)
|
||||
|
||||
.snapshot-date
|
||||
font-size: 0.875rem
|
||||
color: var(--text-dim)
|
||||
margin-top: 0.5rem
|
||||
&:hover
|
||||
border-color: var(--border-strong)
|
||||
box-shadow: 0 4px 12px rgba(31, 36, 41, 0.1)
|
||||
transform: translateY(-1px)
|
||||
|
||||
.disks-section
|
||||
h4
|
||||
font-size: 0.875rem
|
||||
font-weight: 600
|
||||
color: var(--text)
|
||||
margin-bottom: 0.75rem
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
|
||||
&::before
|
||||
content: "💾"
|
||||
font-size: 1rem
|
||||
|
||||
.disk-list
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
.disk-item
|
||||
background: var(--bg-elev)
|
||||
border: 1px solid var(--border)
|
||||
border-radius: var(--radius)
|
||||
padding: 1rem
|
||||
|
||||
.disk-header
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
margin-bottom: 0.75rem
|
||||
|
||||
.disk-serial
|
||||
font-family: 'Courier New', monospace
|
||||
font-size: 0.875rem
|
||||
font-weight: 600
|
||||
color: var(--text)
|
||||
|
||||
.disk-size
|
||||
font-size: 0.875rem
|
||||
color: var(--text-dim)
|
||||
font-weight: 500
|
||||
|
||||
.partitions-section
|
||||
h5
|
||||
font-size: 0.75rem
|
||||
font-weight: 600
|
||||
color: var(--text-dim)
|
||||
text-transform: uppercase
|
||||
letter-spacing: 0.05em
|
||||
margin-bottom: 0.5rem
|
||||
|
||||
.partition-list
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 0.5rem
|
||||
|
||||
.partition-item
|
||||
background: var(--bg-alt)
|
||||
border: 1px solid var(--border)
|
||||
border-radius: var(--radius-sm)
|
||||
padding: 0.75rem
|
||||
|
||||
.partition-header
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
margin-bottom: 0.5rem
|
||||
|
||||
.partition-fs
|
||||
background: var(--accent)
|
||||
color: white
|
||||
font-size: 0.75rem
|
||||
font-weight: 600
|
||||
padding: 0.125rem 0.5rem
|
||||
border-radius: var(--radius-sm)
|
||||
text-transform: uppercase
|
||||
|
||||
.partition-size
|
||||
font-size: 0.75rem
|
||||
color: var(--text-dim)
|
||||
font-weight: 500
|
||||
|
||||
.partition-details
|
||||
display: grid
|
||||
grid-template-columns: 1fr 1fr
|
||||
gap: 0.5rem
|
||||
font-size: 0.75rem
|
||||
color: var(--text-dim)
|
||||
|
||||
.detail-item
|
||||
.snapshot-summary
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: flex-start
|
||||
gap: 1.5rem
|
||||
|
||||
.label
|
||||
font-weight: 500
|
||||
.snapshot-info
|
||||
flex: 1
|
||||
|
||||
.value
|
||||
font-family: 'Courier New', monospace
|
||||
.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
|
||||
|
||||
.empty-snapshots
|
||||
text-align: center
|
||||
padding: 3rem 1rem
|
||||
background: var(--bg-alt)
|
||||
border: 2px dashed var(--border)
|
||||
border-radius: var(--radius-lg)
|
||||
|
||||
.empty-icon
|
||||
font-size: 3rem
|
||||
margin-bottom: 1rem
|
||||
opacity: 0.5
|
||||
.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
|
||||
|
||||
h3
|
||||
font-size: 1.125rem
|
||||
&::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
|
||||
color: var(--text)
|
||||
margin-bottom: 0.5rem
|
||||
letter-spacing: 0.025em
|
||||
|
||||
p
|
||||
color: var(--text-dim)
|
||||
line-height: 1.5
|
||||
|
||||
.loading-section
|
||||
text-align: center
|
||||
padding: 2rem
|
||||
|
||||
.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)
|
||||
|
||||
.error-section
|
||||
text-align: center
|
||||
padding: 2rem
|
||||
background: rgba(217, 48, 37, 0.1)
|
||||
border: 1px solid rgba(217, 48, 37, 0.2)
|
||||
border-radius: var(--radius-lg)
|
||||
|
||||
.error-icon
|
||||
font-size: 2rem
|
||||
color: var(--danger)
|
||||
margin-bottom: 1rem
|
||||
|
||||
h3
|
||||
color: var(--danger)
|
||||
font-size: 1.125rem
|
||||
font-weight: 600
|
||||
margin-bottom: 0.5rem
|
||||
|
||||
p
|
||||
color: var(--text-dim)
|
||||
line-height: 1.5
|
||||
&.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%)
|
||||
|
Reference in New Issue
Block a user