working on system management

This commit is contained in:
Warp Agent
2025-12-26 17:47:20 +00:00
parent 5e63ebc9fe
commit ec0ba85958
19 changed files with 969 additions and 128 deletions

View File

@@ -10,6 +10,7 @@ import TapeLibrariesPage from '@/pages/TapeLibraries'
import VTLDetailPage from '@/pages/VTLDetail'
import ISCSITargetsPage from '@/pages/ISCSITargets'
import ISCSITargetDetailPage from '@/pages/ISCSITargetDetail'
import SystemPage from '@/pages/System'
import Layout from '@/components/Layout'
// Create a client
@@ -55,6 +56,7 @@ function App() {
<Route path="iscsi" element={<ISCSITargetsPage />} />
<Route path="iscsi/:id" element={<ISCSITargetDetailPage />} />
<Route path="alerts" element={<AlertsPage />} />
<Route path="system" element={<SystemPage />} />
</Route>
</Routes>
<Toaster />

View File

@@ -100,6 +100,7 @@ export interface ZFSPool {
scrub_interval: number // days
is_active: boolean
health_status: string // online, degraded, faulted, offline
compress_ratio?: number // compression ratio (e.g., 1.45)
created_at: string
updated_at: string
created_by: string

View File

@@ -0,0 +1,18 @@
import apiClient from './client'
export interface NetworkInterface {
name: string
ip_address: string
subnet: string
status: string // "Connected" or "Down"
speed: string // e.g., "10 Gbps", "1 Gbps"
role: string // "Management", "ISCSI", or empty
}
export const systemAPI = {
listNetworkInterfaces: async (): Promise<NetworkInterface[]> => {
const response = await apiClient.get<{ interfaces: NetworkInterface[] | null }>('/system/interfaces')
return response.data.interfaces || []
},
}

View File

@@ -122,3 +122,23 @@
}
}
/* Custom Toggle Switch */
.toggle-checkbox:checked {
right: 0;
border-color: #137fec;
}
.toggle-checkbox:checked + .toggle-label {
background-color: #137fec;
}
.toggle-checkbox {
right: 0;
left: auto;
}
.toggle-checkbox:checked {
right: 0;
left: auto;
}

View File

@@ -186,8 +186,10 @@ export default function StoragePage() {
const { data: zfsPools = [], isLoading: poolsLoading } = useQuery({
queryKey: ['storage', 'zfs', 'pools'],
queryFn: zfsApi.listPools,
refetchInterval: 2000, // Auto-refresh every 2 seconds
refetchInterval: 3000, // Auto-refresh every 3 seconds
staleTime: 0, // Always consider data stale
refetchOnWindowFocus: true,
refetchOnMount: true,
})
// Fetch ARC stats with auto-refresh every 2 seconds for live data
@@ -254,8 +256,10 @@ export default function StoragePage() {
const deletePoolMutation = useMutation({
mutationFn: (poolId: string) => zfsApi.deletePool(poolId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['storage', 'zfs', 'pools'] })
onSuccess: async () => {
// Invalidate and immediately refetch
await queryClient.invalidateQueries({ queryKey: ['storage', 'zfs', 'pools'] })
queryClient.refetchQueries({ queryKey: ['storage', 'zfs', 'pools'] })
queryClient.invalidateQueries({ queryKey: ['storage', 'disks'] })
setSelectedPool(null)
alert('Pool destroyed successfully!')
@@ -341,20 +345,51 @@ export default function StoragePage() {
const healthyPools = allPools.filter((p) => {
if ('health_status' in p) {
return p.is_active && (p as ZFSPool).health_status === 'online'
const health = (p as ZFSPool).health_status?.toLowerCase() || ''
return p.is_active && health === 'online'
}
return p.is_active
}).length
const degradedPools = allPools.filter((p) => {
if ('health_status' in p) {
return !p.is_active || (p as ZFSPool).health_status !== 'online'
const health = (p as ZFSPool).health_status?.toLowerCase() || ''
return !p.is_active || health !== 'online'
}
return !p.is_active
}).length
const healthStatus = degradedPools === 0 ? 'Optimal' : 'Degraded'
// Mock efficiency data (would come from backend)
const efficiencyRatio = 1.45
// Calculate efficiency ratio from ZFS pools
// Efficiency = average compressratio across all active pools
// Use actual compressratio from ZFS if available, otherwise estimate
const activeZFSPools = zfsPools.filter(p => p.is_active && p.health_status?.toLowerCase() === 'online')
const efficiencyRatio = activeZFSPools.length > 0
? activeZFSPools.reduce((sum, pool) => {
// Use actual compressratio from ZFS if available
if (pool.compress_ratio && pool.compress_ratio > 0) {
// Deduplication can add additional savings (typically 1.2-2x)
const dedupMultiplier = pool.deduplication ? 1.3 : 1.0
return sum + (pool.compress_ratio * dedupMultiplier)
}
// Fallback: estimate based on compression type
const compressionMultiplier: Record<string, number> = {
'lz4': 1.5,
'zstd': 2.5,
'gzip': 2.0,
'gzip-1': 1.8,
'gzip-9': 2.5,
'off': 1.0,
}
const baseRatio = compressionMultiplier[pool.compression?.toLowerCase() || 'lz4'] || 1.5
const dedupMultiplier = pool.deduplication ? 1.3 : 1.0
return sum + (baseRatio * dedupMultiplier)
}, 0) / activeZFSPools.length
: 1.0
// Get compression and deduplication status from pools
const hasCompression = activeZFSPools.some(p => p.compression && p.compression.toLowerCase() !== 'off')
const hasDedup = activeZFSPools.some(p => p.deduplication)
const compressionType = activeZFSPools.find(p => p.compression && p.compression.toLowerCase() !== 'off')?.compression?.toUpperCase() || 'LZ4'
// Use live ARC stats if available, otherwise fallback to 0
const arcHitRatio = arcStats?.hit_ratio ?? 0
const arcCacheUsage = arcStats?.cache_usage ?? 0
@@ -478,8 +513,21 @@ export default function StoragePage() {
<span className="text-xs text-white/70">Ratio</span>
</div>
<div className="flex gap-2 mt-3">
<span className="px-2 py-0.5 rounded bg-blue-500/10 text-blue-500 text-[10px] font-bold">LZ4</span>
<span className="px-2 py-0.5 rounded bg-purple-500/10 text-purple-500 text-[10px] font-bold">DEDUP ON</span>
{hasCompression && (
<span className="px-2 py-0.5 rounded bg-blue-500/10 text-blue-500 text-[10px] font-bold">
{compressionType}
</span>
)}
{hasDedup && (
<span className="px-2 py-0.5 rounded bg-purple-500/10 text-purple-500 text-[10px] font-bold">
DEDUP ON
</span>
)}
{!hasCompression && !hasDedup && (
<span className="px-2 py-0.5 rounded bg-gray-500/10 text-gray-500 text-[10px] font-bold">
NO COMPRESSION
</span>
)}
</div>
</div>
@@ -558,7 +606,7 @@ export default function StoragePage() {
// Check if it's a ZFS pool or LVM repository
const isZFSPool = 'raid_level' in pool
const healthStatus = isZFSPool ? (pool as ZFSPool).health_status : 'online'
const healthStatus = isZFSPool ? ((pool as ZFSPool).health_status?.toLowerCase() || 'online') : 'online'
const isHealthy = pool.is_active && (healthStatus === 'online' || healthStatus === '')
const statusColor = isHealthy
@@ -809,11 +857,11 @@ export default function StoragePage() {
<div className="flex items-center gap-3 mb-3">
<span className="material-symbols-outlined text-primary text-[20px]">info</span>
<span className="text-sm font-bold text-primary">
{selectedPool.is_active && selectedPool.health_status === 'online' ? 'Healthy' : 'Degraded'}
{selectedPool.is_active && selectedPool.health_status?.toLowerCase() === 'online' ? 'Healthy' : 'Degraded'}
</span>
</div>
<p className="text-xs text-white/90 leading-relaxed mb-3">
{selectedPool.is_active && selectedPool.health_status === 'online'
{selectedPool.is_active && selectedPool.health_status?.toLowerCase() === 'online'
? 'This pool is operating normally.'
: 'This pool has issues and requires attention.'}
</p>

View File

@@ -0,0 +1,434 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
import { systemAPI, NetworkInterface } from '@/api/system'
export default function System() {
const [snmpEnabled, setSnmpEnabled] = useState(false)
// Fetch network interfaces
const { data: interfaces = [], isLoading: interfacesLoading } = useQuery({
queryKey: ['system', 'interfaces'],
queryFn: () => systemAPI.listNetworkInterfaces(),
refetchInterval: 5000, // Refresh every 5 seconds
})
return (
<div className="flex-1 flex flex-col h-full overflow-hidden relative bg-background-dark">
{/* Top Navigation */}
<header className="flex h-16 items-center justify-between border-b border-border-dark bg-background-dark px-6 lg:px-10 shrink-0 z-10">
<div className="flex items-center gap-4">
<button className="text-text-secondary md:hidden hover:text-white">
<span className="material-symbols-outlined">menu</span>
</button>
{/* Breadcrumbs */}
<div className="flex items-center gap-2 text-sm">
<Link to="/" className="text-text-secondary hover:text-white transition-colors">
System
</Link>
<span className="text-text-secondary">/</span>
<span className="text-white font-medium">Configuration</span>
</div>
</div>
<div className="flex items-center gap-4">
<div className="hidden md:flex items-center gap-2 px-3 py-1.5 rounded-full bg-green-500/10 border border-green-500/20">
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse"></div>
<span className="text-xs font-medium text-green-500">System Healthy</span>
</div>
<div className="h-6 w-px bg-border-dark mx-2"></div>
<button className="flex items-center justify-center gap-2 rounded-lg bg-border-dark px-4 py-2 text-sm font-bold text-white hover:bg-[#2f455a] transition-colors">
<span className="material-symbols-outlined text-[18px]">restart_alt</span>
<span className="hidden sm:inline">Reboot</span>
</button>
<button className="flex items-center justify-center gap-2 rounded-lg bg-red-500/10 px-4 py-2 text-sm font-bold text-red-500 hover:bg-red-500/20 transition-colors border border-red-500/20">
<span className="material-symbols-outlined text-[18px]">power_settings_new</span>
<span className="hidden sm:inline">Shutdown</span>
</button>
</div>
</header>
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto p-4 md:p-8 lg:px-12 scroll-smooth">
<div className="mx-auto max-w-7xl">
{/* Page Header */}
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight text-white mb-2">System Configuration</h1>
<p className="text-text-secondary text-sm max-w-2xl">
Manage network interfaces, time synchronization, service states, and remote management protocols.
</p>
</div>
<button className="flex items-center justify-center gap-2 rounded-lg bg-primary px-5 py-2.5 text-sm font-bold text-white hover:bg-blue-600 transition-all shadow-lg shadow-blue-500/20">
<span className="material-symbols-outlined text-[20px]">save</span>
Save Changes
</button>
</div>
{/* Grid Layout */}
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
{/* Network Card */}
<div className="flex flex-col rounded-xl border border-border-dark bg-card-dark shadow-sm">
<div className="flex items-center justify-between border-b border-border-dark px-6 py-4">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-primary">lan</span>
<h2 className="text-lg font-bold text-white">Network Interfaces</h2>
</div>
<button className="text-xs font-bold text-primary hover:text-blue-400">CONFIGURE DNS</button>
</div>
<div className="p-2">
{interfacesLoading ? (
<div className="flex items-center justify-center py-8">
<span className="text-text-secondary">Loading interfaces...</span>
</div>
) : interfaces.length === 0 ? (
<div className="flex items-center justify-center py-8">
<span className="text-text-secondary">No network interfaces found</span>
</div>
) : (
interfaces.map((iface: NetworkInterface) => {
const isConnected = iface.status === 'Connected'
const roleBgColor = iface.role === 'ISCSI' ? 'bg-purple-500/20' : 'bg-primary/20'
const roleTextColor = iface.role === 'ISCSI' ? 'text-purple-400' : 'text-primary'
return (
<div
key={iface.name}
className={`group flex items-center justify-between rounded-lg p-3 hover:bg-border-dark/50 transition-colors ${!isConnected ? 'opacity-70' : ''}`}
>
<div className="flex items-center gap-4">
<div className={`flex h-10 w-10 items-center justify-center rounded-lg bg-border-dark ${isConnected ? 'text-white' : 'text-text-secondary'}`}>
<span className="material-symbols-outlined">settings_ethernet</span>
</div>
<div>
<div className="flex items-center gap-2">
<p className={`font-bold ${isConnected ? 'text-white' : 'text-text-secondary'}`}>{iface.name}</p>
{iface.role && (
<span className={`rounded ${roleBgColor} px-1.5 py-0.5 text-[10px] font-bold ${roleTextColor} uppercase`}>
{iface.role}
</span>
)}
</div>
{iface.ip_address ? (
<p className="font-mono text-xs text-text-secondary">
{iface.ip_address} <span className="opacity-50 mx-1">/</span> {iface.subnet}
</p>
) : (
<p className="font-mono text-xs text-text-secondary">No Carrier</p>
)}
</div>
</div>
<div className="flex items-center gap-4">
<div className="hidden sm:flex flex-col items-end">
{isConnected ? (
<>
<div className="flex items-center gap-1.5">
<div className="h-2 w-2 rounded-full bg-green-500"></div>
<span className="text-xs font-medium text-white">Connected</span>
</div>
{iface.speed && iface.speed !== 'Unknown' && (
<span className="text-xs text-text-secondary">{iface.speed}</span>
)}
</>
) : (
<div className="flex items-center gap-1.5">
<div className="h-2 w-2 rounded-full bg-red-500"></div>
<span className="text-xs font-medium text-red-500">Down</span>
</div>
)}
</div>
<button className="h-8 w-8 rounded-full hover:bg-border-dark flex items-center justify-center text-text-secondary hover:text-white transition-colors">
<span className="material-symbols-outlined">more_vert</span>
</button>
</div>
</div>
)
})
)}
</div>
</div>
{/* Services Card */}
<div className="flex flex-col rounded-xl border border-border-dark bg-card-dark shadow-sm">
<div className="flex items-center justify-between border-b border-border-dark px-6 py-4">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-primary">memory</span>
<h2 className="text-lg font-bold text-white">Service Control</h2>
</div>
<div className="flex items-center gap-2">
<span className="h-2 w-2 rounded-full bg-green-500"></span>
<span className="text-xs text-text-secondary">All Systems Normal</span>
</div>
</div>
<div className="p-4 flex flex-col gap-1">
{/* Service Row */}
<div className="flex items-center justify-between rounded-lg bg-[#111a22] p-3 border border-transparent hover:border-border-dark transition-colors">
<div className="flex items-center gap-3">
<div className="p-2 rounded bg-border-dark/50 text-white">
<span className="material-symbols-outlined text-[20px]">terminal</span>
</div>
<div>
<p className="text-sm font-bold text-white">SSH Service</p>
<p className="text-xs text-text-secondary">Remote command line access</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-green-500/20 text-green-500 border border-green-500/20">RUNNING</span>
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input
defaultChecked
className="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer checked:right-0 checked:border-primary transition-all duration-300"
id="ssh-toggle"
name="toggle"
type="checkbox"
/>
<label
className="toggle-label block overflow-hidden h-5 rounded-full bg-border-dark cursor-pointer transition-colors duration-300"
htmlFor="ssh-toggle"
></label>
</div>
</div>
</div>
{/* Service Row */}
<div className="flex items-center justify-between rounded-lg bg-[#111a22] p-3 border border-transparent hover:border-border-dark transition-colors">
<div className="flex items-center gap-3">
<div className="p-2 rounded bg-border-dark/50 text-white">
<span className="material-symbols-outlined text-[20px]">folder_shared</span>
</div>
<div>
<p className="text-sm font-bold text-white">SMB / CIFS</p>
<p className="text-xs text-text-secondary">Windows file sharing</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-green-500/20 text-green-500 border border-green-500/20">RUNNING</span>
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input
defaultChecked
className="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer checked:right-0 checked:border-primary transition-all duration-300"
id="smb-toggle"
name="toggle"
type="checkbox"
/>
<label
className="toggle-label block overflow-hidden h-5 rounded-full bg-border-dark cursor-pointer transition-colors duration-300"
htmlFor="smb-toggle"
></label>
</div>
</div>
</div>
{/* Service Row */}
<div className="flex items-center justify-between rounded-lg bg-[#111a22] p-3 border border-transparent hover:border-border-dark transition-colors">
<div className="flex items-center gap-3">
<div className="p-2 rounded bg-border-dark/50 text-white">
<span className="material-symbols-outlined text-[20px]">storage</span>
</div>
<div>
<p className="text-sm font-bold text-white">iSCSI Target</p>
<p className="text-xs text-text-secondary">Block storage sharing</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-yellow-500/20 text-yellow-500 border border-yellow-500/20">STOPPED</span>
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input
className="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer checked:right-0 checked:border-primary transition-all duration-300"
id="iscsi-toggle"
name="toggle"
type="checkbox"
/>
<label
className="toggle-label block overflow-hidden h-5 rounded-full bg-border-dark cursor-pointer transition-colors duration-300"
htmlFor="iscsi-toggle"
></label>
</div>
</div>
</div>
{/* Service Row */}
<div className="flex items-center justify-between rounded-lg bg-[#111a22] p-3 border border-transparent hover:border-border-dark transition-colors">
<div className="flex items-center gap-3">
<div className="p-2 rounded bg-border-dark/50 text-white">
<span className="material-symbols-outlined text-[20px]">share</span>
</div>
<div>
<p className="text-sm font-bold text-white">NFS Service</p>
<p className="text-xs text-text-secondary">Unix file sharing</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-green-500/20 text-green-500 border border-green-500/20">RUNNING</span>
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input
defaultChecked
className="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer checked:right-0 checked:border-primary transition-all duration-300"
id="nfs-toggle"
name="toggle"
type="checkbox"
/>
<label
className="toggle-label block overflow-hidden h-5 rounded-full bg-border-dark cursor-pointer transition-colors duration-300"
htmlFor="nfs-toggle"
></label>
</div>
</div>
</div>
{/* Service Row - VTL (MHVTL) */}
<div className="flex items-center justify-between rounded-lg bg-[#111a22] p-3 border border-transparent hover:border-border-dark transition-colors">
<div className="flex items-center gap-3">
<div className="p-2 rounded bg-border-dark/50 text-white">
<span className="material-symbols-outlined text-[20px]">album</span>
</div>
<div>
<p className="text-sm font-bold text-white">VTL Service</p>
<p className="text-xs text-text-secondary">Virtual tape library emulation</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className="px-2 py-0.5 rounded text-[10px] font-bold bg-green-500/20 text-green-500 border border-green-500/20">RUNNING</span>
<div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input
defaultChecked
className="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer checked:right-0 checked:border-primary transition-all duration-300"
id="mhvtl-toggle"
name="toggle"
type="checkbox"
/>
<label
className="toggle-label block overflow-hidden h-5 rounded-full bg-border-dark cursor-pointer transition-colors duration-300"
htmlFor="mhvtl-toggle"
></label>
</div>
</div>
</div>
</div>
</div>
{/* Date & Time Card */}
<div className="flex flex-col rounded-xl border border-border-dark bg-card-dark shadow-sm">
<div className="flex items-center justify-between border-b border-border-dark px-6 py-4">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-primary">schedule</span>
<h2 className="text-lg font-bold text-white">Date & Time</h2>
</div>
<span className="text-xs font-mono text-text-secondary bg-border-dark px-2 py-1 rounded">UTC</span>
</div>
<div className="p-6 flex flex-col gap-6">
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className="mb-2 block text-xs font-medium text-text-secondary uppercase">System Timezone</label>
<div className="relative">
<select className="block w-full rounded-lg border-border-dark bg-[#111a22] py-2.5 pl-3 pr-10 text-sm text-white focus:border-primary focus:ring-1 focus:ring-primary appearance-none">
<option>Etc/UTC</option>
<option>America/New_York</option>
<option>Europe/London</option>
<option>Asia/Tokyo</option>
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-white">
<span className="material-symbols-outlined text-sm">expand_more</span>
</div>
</div>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-xs font-medium text-text-secondary uppercase">NTP Servers</label>
<button className="text-xs text-primary font-bold hover:text-white flex items-center gap-1">
<span className="material-symbols-outlined text-[14px]">add</span> Add Server
</button>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between rounded-lg bg-[#111a22] p-3 border border-border-dark">
<div className="flex items-center gap-3">
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse"></div>
<span className="text-sm font-mono text-white">pool.ntp.org</span>
</div>
<span className="text-xs text-text-secondary">Stratum 2 12ms</span>
</div>
<div className="flex items-center justify-between rounded-lg bg-[#111a22] p-3 border border-border-dark">
<div className="flex items-center gap-3">
<div className="h-2 w-2 rounded-full bg-green-500"></div>
<span className="text-sm font-mono text-white">time.google.com</span>
</div>
<span className="text-xs text-text-secondary">Stratum 1 45ms</span>
</div>
</div>
</div>
</div>
</div>
{/* Management & SNMP Card */}
<div className="flex flex-col rounded-xl border border-border-dark bg-card-dark shadow-sm">
<div className="flex items-center justify-between border-b border-border-dark px-6 py-4">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-primary">hub</span>
<h2 className="text-lg font-bold text-white">Management</h2>
</div>
</div>
<div className="p-6 flex flex-col gap-6">
<div>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-sm font-bold text-white">SNMP Monitoring</h3>
<p className="text-xs text-text-secondary">Enable Simple Network Management Protocol</p>
</div>
<div className="relative inline-block w-10 align-middle select-none transition duration-200 ease-in">
<input
checked={snmpEnabled}
onChange={(e) => setSnmpEnabled(e.target.checked)}
className="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer checked:right-0 checked:border-primary transition-all duration-300"
id="snmp-toggle"
name="toggle"
type="checkbox"
/>
<label
className="toggle-label block overflow-hidden h-5 rounded-full bg-border-dark cursor-pointer transition-colors duration-300"
htmlFor="snmp-toggle"
></label>
</div>
</div>
<div className={`grid grid-cols-1 gap-4 transition-opacity ${snmpEnabled ? 'opacity-100' : 'opacity-50 pointer-events-none'}`}>
<div>
<label className="mb-1.5 block text-xs font-medium text-text-secondary uppercase">Community String</label>
<input
className="block w-full rounded-lg border-border-dark bg-[#111a22] p-2.5 text-sm text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
placeholder="e.g. public"
type="text"
defaultValue="public"
disabled={!snmpEnabled}
/>
</div>
<div>
<label className="mb-1.5 block text-xs font-medium text-text-secondary uppercase">Trap Receiver IP</label>
<input
className="block w-full rounded-lg border-border-dark bg-[#111a22] p-2.5 text-sm text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
placeholder="e.g. 192.168.1.100"
type="text"
disabled={!snmpEnabled}
/>
</div>
</div>
</div>
<div className="border-t border-border-dark pt-4">
<h3 className="text-sm font-bold text-white mb-3">Syslog Forwarding</h3>
<div className="flex gap-2">
<input
className="flex-1 rounded-lg border-border-dark bg-[#111a22] p-2.5 text-sm text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
placeholder="Syslog Server Address (UDP:514)"
type="text"
/>
<button className="rounded-lg bg-border-dark px-4 py-2 text-sm font-bold text-white hover:bg-[#2f455a] transition-colors">
Test
</button>
</div>
</div>
</div>
</div>
</div>
{/* Bottom Spacer */}
<div className="h-10"></div>
</div>
</div>
</div>
)
}

View File

@@ -460,10 +460,10 @@ export default function TapeLibraries() {
</div>
{/* Tape Detail Drawer */}
{selectedLibrary && activeTab === 'vtl' && libraryTapes.length > 0 && (
<div className="bg-surface-dark border-t border-border-dark p-6 absolute bottom-0 w-full transform translate-y-0 transition-transform z-30 shadow-2xl shadow-black">
{selectedLibrary && activeTab === 'vtl' && (
<div className="bg-surface-dark border-t border-border-dark p-6 absolute bottom-0 w-full transform translate-y-0 transition-transform z-30 shadow-2xl shadow-black max-h-[70vh] overflow-y-auto">
<div className="max-w-[1400px] mx-auto">
<div className="flex justify-between items-center mb-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-primary text-2xl">cable</span>
<div>
@@ -475,7 +475,7 @@ export default function TapeLibraries() {
</p>
</div>
</div>
<div className="flex gap-3">
<div className="flex gap-3 flex-wrap">
<button className="px-3 py-2 bg-[#111a22] border border-border-dark rounded-lg text-text-secondary hover:text-white text-sm font-medium transition-colors">
Bulk Format
</button>
@@ -488,47 +488,71 @@ export default function TapeLibraries() {
</Link>
<button
onClick={() => setSelectedLibrary(null)}
className="lg:hidden p-2 text-text-secondary hover:text-white"
className="p-2 text-text-secondary hover:text-white"
>
<span className="material-symbols-outlined">close</span>
</button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
{libraryTapes.map((tape) => (
<div
key={tape.id}
className={`p-3 rounded border flex flex-col gap-2 relative group hover:border-primary transition-colors cursor-pointer ${
tape.status === 'in_drive'
? 'bg-[#111a22] border-green-500/30'
: 'bg-[#111a22] border-border-dark'
}`}
{libraryTapes.length === 0 ? (
<div className="text-center py-12">
<span className="material-symbols-outlined text-6xl text-text-secondary mb-4 block">album</span>
<h3 className="text-lg font-medium text-white mb-2">No Tapes Found</h3>
<p className="text-sm text-text-secondary mb-4">
This library has no tapes yet. Create tapes to get started.
</p>
<Link
to={`/tape/vtl/${selectedLibrary}/tapes/create`}
className="inline-flex items-center gap-2 px-4 py-2 bg-primary hover:bg-blue-600 rounded-lg text-white text-sm font-bold"
>
<div className="flex justify-between items-start">
<span
className={`material-symbols-outlined text-xl ${
tape.status === 'in_drive' ? 'text-green-500' : 'text-text-secondary'
}`}
>
album
</span>
<span className="text-[10px] uppercase font-bold text-text-secondary bg-[#1c2834] px-1 rounded">
Slot {tape.slot_number}
</span>
<span className="material-symbols-outlined text-lg">add</span>
Add Tapes
</Link>
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-3">
{libraryTapes.map((tape) => (
<div
key={tape.id}
className={`p-3 rounded-lg border flex flex-col gap-2 relative group hover:border-primary transition-all cursor-pointer min-h-[120px] ${
tape.status === 'in_drive'
? 'bg-[#111a22] border-green-500/30 shadow-lg shadow-green-500/10'
: 'bg-[#111a22] border-border-dark hover:shadow-lg hover:shadow-primary/10'
}`}
>
<div className="flex justify-between items-start">
<span
className={`material-symbols-outlined text-xl ${
tape.status === 'in_drive' ? 'text-green-500' : 'text-text-secondary'
}`}
>
album
</span>
<span className="text-[10px] uppercase font-bold text-text-secondary bg-[#1c2834] px-1.5 py-0.5 rounded">
SLOT {tape.slot_number}
</span>
</div>
<div className="flex-1 flex flex-col justify-center">
<p className="text-white text-xs font-mono font-bold truncate" title={tape.barcode}>
{tape.barcode}
</p>
<p className="text-text-secondary text-[10px] mt-1">
{formatBytes(tape.size_bytes, 1)} / {formatBytes(tape.size_bytes, 1)}
</p>
</div>
<div className="absolute inset-0 bg-black/70 hidden group-hover:flex items-center justify-center gap-2 backdrop-blur-sm rounded-lg">
<button className="p-2 text-white hover:text-primary hover:bg-primary/20 rounded transition-colors" title="Eject">
<span className="material-symbols-outlined text-lg">eject</span>
</button>
<button className="p-2 text-white hover:text-red-400 hover:bg-red-400/20 rounded transition-colors" title="Delete">
<span className="material-symbols-outlined text-lg">delete</span>
</button>
</div>
</div>
<div>
<p className="text-white text-xs font-mono font-bold">{tape.barcode}</p>
<p className="text-text-secondary text-[10px]">
{formatBytes(tape.size_bytes, 1)} / {formatBytes(tape.size_bytes, 1)}
</p>
</div>
<div className="absolute inset-0 bg-black/60 hidden group-hover:flex items-center justify-center gap-2 backdrop-blur-[1px] rounded">
<span className="material-symbols-outlined text-white hover:text-primary text-lg">eject</span>
<span className="material-symbols-outlined text-white hover:text-red-400 text-lg">delete</span>
</div>
</div>
))}
</div>
))}
</div>
)}
</div>
</div>
)}

View File

@@ -42,9 +42,9 @@ export default {
foreground: "hsl(var(--card-foreground))",
},
// Dark theme colors from example
"background-dark": "#111a22",
"card-dark": "#1a2632",
"border-dark": "#324d67",
"background-dark": "#101922",
"card-dark": "#192633",
"border-dark": "#233648",
"text-secondary": "#92adc9",
},
fontFamily: {