Files
calypso/frontend/src/pages/VTLDetail.tsx
2025-12-25 09:01:49 +00:00

318 lines
12 KiB
TypeScript

import { useParams, useNavigate } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { vtlAPI, type TapeDrive, type VirtualTape } from '@/api/tape'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { ArrowLeft, Trash2, Plus, Play, RefreshCw } from 'lucide-react'
import { formatBytes } from '@/lib/format'
import { useState } from 'react'
export default function VTLDetail() {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const queryClient = useQueryClient()
const [selectedDrive, setSelectedDrive] = useState<string | null>(null)
const { data, isLoading } = useQuery({
queryKey: ['vtl-library', id],
queryFn: () => vtlAPI.getLibrary(id!),
enabled: !!id,
})
const deleteMutation = useMutation({
mutationFn: () => vtlAPI.deleteLibrary(id!),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['vtl-libraries'] })
navigate('/tape')
},
})
if (isLoading) {
return <div className="text-sm text-text-secondary min-h-screen bg-background-dark p-6">Loading library details...</div>
}
if (!data) {
return <div className="text-sm text-red-400 min-h-screen bg-background-dark p-6">Library not found</div>
}
const { library, drives, tapes } = data
return (
<div className="space-y-6 min-h-screen bg-background-dark p-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Button variant="ghost" size="sm" onClick={() => navigate('/tape')}>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
<div>
<h1 className="text-3xl font-bold text-white">{library.name}</h1>
<p className="mt-1 text-sm text-text-secondary">
Virtual Tape Library {library.slot_count} slots {library.drive_count} drives
</p>
</div>
</div>
<Button
variant="destructive"
onClick={() => {
if (confirm('Are you sure you want to delete this library?')) {
deleteMutation.mutate()
}
}}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Library
</Button>
</div>
{/* Library Info */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">Library Status</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-text-secondary">Status:</span>
<span className={library.is_active ? 'text-green-400' : 'text-text-secondary'}>
{library.is_active ? 'Active' : 'Inactive'}
</span>
</div>
<div className="flex justify-between">
<span className="text-text-secondary">mhVTL ID:</span>
<span className="font-medium text-white">{library.mhvtl_library_id}</span>
</div>
<div className="flex justify-between">
<span className="text-text-secondary">Storage Path:</span>
<span className="font-mono text-xs text-white">{library.storage_path}</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">Capacity</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-text-secondary">Total Slots:</span>
<span className="font-medium text-white">{library.slot_count}</span>
</div>
<div className="flex justify-between">
<span className="text-text-secondary">Used Slots:</span>
<span className="font-medium text-white">{tapes.length}</span>
</div>
<div className="flex justify-between">
<span className="text-text-secondary">Free Slots:</span>
<span className="font-medium text-white">{library.slot_count - tapes.length}</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">Drives</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-text-secondary">Total Drives:</span>
<span className="font-medium text-white">{library.drive_count}</span>
</div>
<div className="flex justify-between">
<span className="text-text-secondary">Idle:</span>
<span className="font-medium text-white">
{drives.filter((d) => d.status === 'idle').length}
</span>
</div>
<div className="flex justify-between">
<span className="text-text-secondary">Ready:</span>
<span className="font-medium text-white">
{drives.filter((d) => d.status === 'ready').length}
</span>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Drives */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Drives</CardTitle>
<CardDescription>Virtual tape drives in this library</CardDescription>
</div>
<Button variant="outline" size="sm">
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
</div>
</CardHeader>
<CardContent>
{drives.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{drives.map((drive) => (
<DriveCard
key={drive.id}
drive={drive}
tapes={tapes}
isSelected={selectedDrive === drive.id}
onSelect={() => setSelectedDrive(drive.id)}
/>
))}
</div>
) : (
<p className="text-sm text-text-secondary">No drives configured</p>
)}
</CardContent>
</Card>
{/* Tapes */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Virtual Tapes</CardTitle>
<CardDescription>
{tapes.length} of {library.slot_count} slots used
</CardDescription>
</div>
<Button variant="outline" size="sm">
<Plus className="h-4 w-4 mr-2" />
Create Tape
</Button>
</div>
</CardHeader>
<CardContent>
{tapes.length > 0 ? (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-[#1a2632]">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Barcode
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Slot
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Size
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-text-secondary uppercase">
Actions
</th>
</tr>
</thead>
<tbody className="bg-card-dark divide-y divide-border-dark">
{tapes.map((tape) => (
<tr key={tape.id} className="hover:bg-[#233648]">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-white">
{tape.barcode}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">
{tape.slot_number}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary">
{formatBytes(tape.size_bytes)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 text-xs font-medium rounded ${
tape.status === 'loaded'
? 'bg-blue-100 text-blue-800'
: tape.status === 'mounted'
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{tape.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{selectedDrive && (
<Button
variant="outline"
size="sm"
onClick={() => {
// TODO: Implement load tape
console.log('Load tape', tape.id, 'to drive', selectedDrive)
}}
>
<Play className="h-3 w-3 mr-1" />
Load
</Button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-sm text-text-secondary">No tapes created yet</p>
)}
</CardContent>
</Card>
</div>
)
}
interface DriveCardProps {
drive: TapeDrive
tapes: VirtualTape[]
isSelected: boolean
onSelect: () => void
}
function DriveCard({ drive, tapes, isSelected, onSelect }: DriveCardProps) {
const currentTape = tapes.find((t) => t.id === drive.current_tape_id)
return (
<div
className={`border rounded-lg p-4 cursor-pointer transition-colors ${
isSelected ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300'
}`}
onClick={onSelect}
>
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium">Drive {drive.drive_number}</h4>
<span
className={`px-2 py-1 text-xs font-medium rounded ${
drive.status === 'ready'
? 'bg-green-100 text-green-800'
: drive.status === 'loaded'
? 'bg-blue-100 text-blue-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{drive.status}
</span>
</div>
{currentTape ? (
<div className="text-sm text-text-secondary">
<p className="text-white">Tape: {currentTape.barcode}</p>
<p className="text-xs text-text-secondary">{formatBytes(currentTape.size_bytes)}</p>
</div>
) : (
<p className="text-sm text-text-secondary">No tape loaded</p>
)}
{isSelected && (
<div className="mt-2 pt-2 border-t">
<p className="text-xs text-blue-600">Selected - Click tape to load</p>
</div>
)}
</div>
)
}