still working on frontend UI
This commit is contained in:
317
frontend/src/pages/VTLDetail.tsx
Normal file
317
frontend/src/pages/VTLDetail.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
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-gray-500">Loading library details...</div>
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <div className="text-sm text-red-500">Library not found</div>
|
||||
}
|
||||
|
||||
const { library, drives, tapes } = data
|
||||
|
||||
return (
|
||||
<div className="space-y-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-gray-900">{library.name}</h1>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
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-gray-500">Status:</span>
|
||||
<span className={library.is_active ? 'text-green-600' : 'text-gray-600'}>
|
||||
{library.is_active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">mhVTL ID:</span>
|
||||
<span className="font-medium">{library.mhvtl_library_id}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Storage Path:</span>
|
||||
<span className="font-mono text-xs">{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-gray-500">Total Slots:</span>
|
||||
<span className="font-medium">{library.slot_count}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Used Slots:</span>
|
||||
<span className="font-medium">{tapes.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Free Slots:</span>
|
||||
<span className="font-medium">{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-gray-500">Total Drives:</span>
|
||||
<span className="font-medium">{library.drive_count}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Idle:</span>
|
||||
<span className="font-medium">
|
||||
{drives.filter((d) => d.status === 'idle').length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Ready:</span>
|
||||
<span className="font-medium">
|
||||
{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-gray-500">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-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Barcode
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Slot
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Size
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{tapes.map((tape) => (
|
||||
<tr key={tape.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
{tape.barcode}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{tape.slot_number}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{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-gray-500">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-gray-600">
|
||||
<p>Tape: {currentTape.barcode}</p>
|
||||
<p className="text-xs text-gray-500">{formatBytes(currentTape.size_bytes)}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-400">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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user