import { useParams, useNavigate } from 'react-router-dom' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { scstAPI, type SCSTTarget, type SCSTExtent } from '@/api/scst' import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { ArrowLeft, Plus, RefreshCw, HardDrive, Users, Trash2 } from 'lucide-react' import { useState, useEffect } from 'react' export default function ISCSITargetDetail() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const queryClient = useQueryClient() const [showAddLUN, setShowAddLUN] = useState(false) const [showAddInitiator, setShowAddInitiator] = useState(false) useEffect(() => { console.log('showAddLUN state:', showAddLUN) }, [showAddLUN]) const { data, isLoading } = useQuery({ queryKey: ['scst-target', id], queryFn: () => scstAPI.getTarget(id!), enabled: !!id, }) const removeLUNMutation = useMutation({ mutationFn: ({ targetId, lunId }: { targetId: string; lunId: string }) => scstAPI.removeLUN(targetId, lunId), onMutate: async ({ lunId }) => { // Cancel any outgoing refetches await queryClient.cancelQueries({ queryKey: ['scst-target', id] }) await queryClient.cancelQueries({ queryKey: ['scst-targets'] }) // Snapshot the previous value const previousTarget = queryClient.getQueryData(['scst-target', id]) const previousTargets = queryClient.getQueryData(['scst-targets']) // Optimistically update to remove the LUN queryClient.setQueryData(['scst-target', id], (old: any) => { if (!old) return old return { ...old, luns: old.luns ? old.luns.filter((lun: any) => lun.id !== lunId) : [] } }) // Optimistically update LUN count in targets list queryClient.setQueryData(['scst-targets'], (old) => { if (!old) return old return old.map(t => t.id === id ? { ...t, lun_count: Math.max(0, (t.lun_count || 0) - 1) } : t ) }) return { previousTarget, previousTargets } }, onSuccess: () => { // Invalidate queries to refetch data from the server. // This is simpler and less prone to race conditions than the previous implementation. queryClient.invalidateQueries({ queryKey: ['scst-target', id] }); queryClient.invalidateQueries({ queryKey: ['scst-targets'] }); }, onError: (error: any, _variables, context) => { // If 404, treat as success (LUN already deleted) if (error.response?.status === 404) { // LUN already deleted, just refresh to sync UI queryClient.invalidateQueries({ queryKey: ['scst-target', id] }); queryClient.invalidateQueries({ queryKey: ['scst-targets'] }); return } // Rollback optimistic update if (context?.previousTarget) { queryClient.setQueryData(['scst-target', id], context.previousTarget) } if (context?.previousTargets) { queryClient.setQueryData(['scst-targets'], context.previousTargets) } alert(`Failed to remove LUN: ${error.response?.data?.error || error.message}`) }, }) if (isLoading) { return
Loading target details...
} if (!data) { return
Target not found
} const { target, luns } = data // Ensure luns is always an array, not null const lunsArray = luns || [] return (
{/* Header */}

{target.iqn}

{target.alias && (

{target.alias}

)}
{/* Target Info */}
Target Status
Status: {target.is_active ? 'Active' : 'Inactive'}
IQN: {target.iqn}
LUNs
Total LUNs: {lunsArray.length}
Active: {lunsArray.filter((l) => l.is_active).length}
Actions
{/* LUNs */}
LUNs (Logical Unit Numbers) Storage devices exported by this target
{lunsArray.length > 0 ? (
{lunsArray.map((lun) => ( ))}
LUN # Handler Device Path Type Status Actions
{lun.lun_number} {lun.handler} {lun.device_path} {lun.device_type} {lun.is_active ? 'Active' : 'Inactive'}
) : (

No LUNs configured

)}
{/* Assign Extent Form */} {showAddLUN && ( setShowAddLUN(false)} onSuccess={async () => { setShowAddLUN(false) // Invalidate queries to refetch data. // Invalidate extents since one is now in use. queryClient.invalidateQueries({ queryKey: ['scst-target', id] }); queryClient.invalidateQueries({ queryKey: ['scst-extents'] }); }} /> )} {/* Add Initiator Form */} {showAddInitiator && ( setShowAddInitiator(false)} onSuccess={() => { setShowAddInitiator(false) queryClient.invalidateQueries({ queryKey: ['scst-target', id] }) }} /> )}
) } interface AssignExtentFormProps { targetId: string onClose: () => void onSuccess: () => Promise } function AssignExtentForm({ targetId, onClose, onSuccess }: AssignExtentFormProps) { const [selectedExtent, setSelectedExtent] = useState('') const [lunNumber, setLunNumber] = useState(0) // Fetch available extents const { data: extents = [], isLoading: extentsLoading } = useQuery({ queryKey: ['scst-extents'], queryFn: scstAPI.listExtents, staleTime: 0, refetchOnMount: true, }) // Filter only extents that are not in use const availableExtents = extents.filter(extent => !extent.is_in_use) const addLUNMutation = useMutation({ mutationFn: (data: { device_name: string; device_path: string; lun_number: number; handler_type: string }) => scstAPI.addLUN(targetId, data), onSuccess: async () => { await onSuccess() }, onError: (error: any) => { const errorMessage = error.response?.data?.error || error.message || 'Failed to assign extent' alert(errorMessage) }, }) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (!selectedExtent || lunNumber < 0) { alert('Please select an extent and specify LUN number') return } const extent = availableExtents.find(e => e.device_name === selectedExtent) if (!extent) { alert('Selected extent not found') return } addLUNMutation.mutate({ device_name: extent.device_name, device_path: extent.device_path, handler_type: extent.handler_type, lun_number: lunNumber, }) } return (

Assign Extent

Assign an existing extent to this target as a LUN

{extentsLoading ? (
Loading extents...
) : availableExtents.length === 0 ? (
No available extents. Please create an extent first in the Extents tab.
) : ( )}

Select an extent that has been created in the Extents tab

{selectedExtent && (

Extent Details:

{(() => { const extent = availableExtents.find(e => e.device_name === selectedExtent) if (!extent) return null return (
Device Name: {extent.device_name}
Handler: {extent.handler_type}
Path: {extent.device_path}
) })()}
)}
setLunNumber(parseInt(e.target.value) || 0)} min="0" max="255" className="w-full px-3 py-2 bg-[#0f161d] border border-border-dark rounded-lg text-white text-sm focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary" required />

Logical Unit Number (0-255, typically start from 0)

) } interface AddInitiatorFormProps { targetId: string onClose: () => void onSuccess: () => void } function AddInitiatorForm({ targetId, onClose, onSuccess }: AddInitiatorFormProps) { const [iqn, setIqn] = useState('') const addInitiatorMutation = useMutation({ mutationFn: (data: { initiator_iqn: string }) => scstAPI.addInitiator(targetId, data), onSuccess: () => { onSuccess() }, }) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (!iqn) { alert('Initiator IQN is required') return } addInitiatorMutation.mutate({ initiator_iqn: iqn.trim(), }) } return ( Add Initiator Allow an iSCSI initiator to access this target
setIqn(e.target.value)} placeholder="iqn.2024-01.com.client:initiator1" className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 font-mono text-sm" required />

Format: iqn.YYYY-MM.reverse.domain:identifier

) }