fixing storage management dashboard

This commit is contained in:
Warp Agent
2025-12-25 20:02:59 +00:00
parent a5e6197bca
commit 419fcb7625
20 changed files with 3229 additions and 396 deletions

View File

@@ -155,7 +155,12 @@ export const zfsApi = {
},
deleteDataset: async (poolId: string, datasetName: string): Promise<void> => {
await apiClient.delete(`/storage/zfs/pools/${poolId}/datasets/${datasetName}`)
await apiClient.delete(`/storage/zfs/pools/${poolId}/datasets/${encodeURIComponent(datasetName)}`)
},
getARCStats: async (): Promise<ARCStats> => {
const response = await apiClient.get<ARCStats>('/storage/zfs/arc/stats')
return response.data
},
}
@@ -174,3 +179,17 @@ export interface ZFSDataset {
created_at: string
}
export interface ARCStats {
hit_ratio: number
cache_usage: number
cache_size: number
cache_max: number
hits: number
misses: number
demand_hits: number
prefetch_hits: number
mru_hits: number
mfu_hits: number
collected_at: string
}

View File

@@ -11,7 +11,7 @@ export function useWebSocket(url: string) {
const [isConnected, setIsConnected] = useState(false)
const [lastMessage, setLastMessage] = useState<WebSocketEvent | null>(null)
const wsRef = useRef<WebSocket | null>(null)
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout>>()
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
const { token } = useAuthStore()
useEffect(() => {

View File

@@ -1,16 +1,20 @@
import React, { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import { storageApi, Repository, zfsApi, ZFSPool, PhysicalDisk } from '@/api/storage'
import { storageApi, Repository, zfsApi, ZFSPool, PhysicalDisk, ZFSDataset } from '@/api/storage'
import { formatBytes } from '@/lib/format'
// Component to render dataset rows for a pool
function DatasetRows({ poolId, onDeleteDataset, onCreateDataset }: { poolId: string; onDeleteDataset: (poolId: string, datasetName: string) => void; onCreateDataset: (poolId: string) => void }) {
const { data: datasets = [], isLoading } = useQuery({
queryKey: ['storage', 'zfs', 'pools', poolId, 'datasets'],
const queryKey = ['storage', 'zfs', 'pools', poolId, 'datasets']
const { data: datasets = [], isLoading } = useQuery<ZFSDataset[]>({
queryKey: queryKey,
queryFn: () => zfsApi.listDatasets(poolId),
refetchOnWindowFocus: true,
refetchOnMount: true,
staleTime: 0, // Always consider data stale to force refetch
refetchInterval: 1000, // Auto-refresh every 1 second
})
if (isLoading) {
@@ -70,19 +74,28 @@ function DatasetRows({ poolId, onDeleteDataset, onCreateDataset }: { poolId: str
</span>
<div className="flex flex-col">
<span className="font-medium text-white">{datasetDisplayName}</span>
{dataset.mount_point && dataset.mount_point !== 'none' && (
{dataset.mount_point && dataset.mount_point !== 'none' && dataset.mount_point !== '-' && (
<span className="text-xs text-white/50">{dataset.mount_point}</span>
)}
{dataset.type === 'volume' && (
<span className="text-xs text-primary/70">Volume (Block Device)</span>
)}
</div>
</div>
</td>
<td className="py-3 px-5">
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-bold ${
dataset.mount_point && dataset.mount_point !== 'none'
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
: 'bg-gray-500/20 text-gray-400 border border-gray-500/30'
dataset.type === 'volume'
? 'bg-blue-500/20 text-blue-400 border border-blue-500/30'
: dataset.mount_point && dataset.mount_point !== 'none' && dataset.mount_point !== '-'
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
: 'bg-gray-500/20 text-gray-400 border border-gray-500/30'
}`}>
{dataset.mount_point && dataset.mount_point !== 'none' ? 'MOUNTED' : 'UNMOUNTED'}
{dataset.type === 'volume'
? 'VOLUME'
: dataset.mount_point && dataset.mount_point !== 'none' && dataset.mount_point !== '-'
? 'MOUNTED'
: 'UNMOUNTED'}
</span>
</td>
<td className="py-3 px-5">
@@ -175,6 +188,14 @@ export default function StoragePage() {
queryFn: zfsApi.listPools,
})
// Fetch ARC stats with auto-refresh every 2 seconds for live data
const { data: arcStats } = useQuery({
queryKey: ['storage', 'zfs', 'arc', 'stats'],
queryFn: zfsApi.getARCStats,
refetchInterval: 2000, // Refresh every 2 seconds for live data
staleTime: 0,
})
const syncDisksMutation = useMutation({
mutationFn: storageApi.syncDisks,
@@ -224,12 +245,11 @@ export default function StoragePage() {
const createDatasetMutation = useMutation({
mutationFn: ({ poolId, data }: { poolId: string; data: any }) =>
zfsApi.createDataset(poolId, data),
onSuccess: (_, variables) => {
// Invalidate queries BEFORE resetting state
queryClient.invalidateQueries({ queryKey: ['storage', 'zfs', 'pools'] })
queryClient.invalidateQueries({ queryKey: ['storage', 'zfs', 'pools', variables.poolId, 'datasets'] })
// Also invalidate all dataset queries for this pool
queryClient.invalidateQueries({ queryKey: ['storage', 'zfs', 'pools', variables.poolId] })
onSuccess: async (_, variables) => {
// Ensure pool is expanded to show the new dataset
setExpandedPools(prev => new Set(prev).add(variables.poolId))
// Close modal and reset form
setShowCreateDatasetModal(false)
setSelectedPoolForDataset(null)
setDatasetForm({
@@ -240,6 +260,14 @@ export default function StoragePage() {
reservation: '',
mount_point: '',
})
// Simply invalidate query - React Query will automatically refetch
// Backend already saved to database, so next query will get fresh data
queryClient.invalidateQueries({
queryKey: ['storage', 'zfs', 'pools', variables.poolId, 'datasets'],
exact: true
})
alert('Dataset created successfully!')
},
onError: (error: any) => {
@@ -251,9 +279,16 @@ export default function StoragePage() {
const deleteDatasetMutation = useMutation({
mutationFn: ({ poolId, datasetName }: { poolId: string; datasetName: string }) =>
zfsApi.deleteDataset(poolId, datasetName),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['storage', 'zfs', 'pools'] })
queryClient.invalidateQueries({ queryKey: ['storage', 'zfs', 'pools', variables.poolId, 'datasets'] })
onSuccess: async (_, variables) => {
// Ensure pool is expanded to show updated list
setExpandedPools(prev => new Set(prev).add(variables.poolId))
// Simply invalidate and refetch - backend already removed from database
await queryClient.invalidateQueries({
queryKey: ['storage', 'zfs', 'pools', variables.poolId, 'datasets'],
exact: true
})
alert('Dataset deleted successfully!')
},
onError: (error: any) => {
@@ -296,7 +331,11 @@ export default function StoragePage() {
// Mock efficiency data (would come from backend)
const efficiencyRatio = 1.45
const arcHitRatio = 98.2
// Use live ARC stats if available, otherwise fallback to 0
const arcHitRatio = arcStats?.hit_ratio ?? 0
const arcCacheUsage = arcStats?.cache_usage ?? 0
const arcCacheSize = arcStats?.cache_size ?? 0
const arcCacheMax = arcStats?.cache_max ?? 0
const togglePool = (poolId: string) => {
const newExpanded = new Set(expandedPools)
@@ -425,9 +464,11 @@ export default function StoragePage() {
<span className="material-symbols-outlined text-white/70">memory</span>
</div>
<div className="flex items-baseline gap-2">
<h3 className="text-2xl font-bold text-white">{arcHitRatio}%</h3>
<h3 className="text-2xl font-bold text-white">{arcHitRatio.toFixed(1)}%</h3>
</div>
<p className="mt-2 text-xs text-white/80">Cache Usage: N/A</p>
<p className="mt-2 text-xs text-white/80">
Cache Usage: {arcCacheMax > 0 ? `${formatBytes(arcCacheSize, 1)} / ${formatBytes(arcCacheMax, 1)} (${arcCacheUsage.toFixed(1)}%)` : 'N/A'}
</p>
<div className="mt-2 flex gap-1">
<div className="h-1 flex-1 bg-emerald-500 rounded-full"></div>
<div className="h-1 flex-1 bg-emerald-500 rounded-full"></div>
@@ -600,7 +641,11 @@ export default function StoragePage() {
</tr>
{/* Child Datasets (if expanded) */}
{isExpanded && isZFSPool && (
<DatasetRows poolId={pool.id} onDeleteDataset={handleDeleteDataset} onCreateDataset={handleCreateDataset} />
<DatasetRows
poolId={pool.id}
onDeleteDataset={handleDeleteDataset}
onCreateDataset={handleCreateDataset}
/>
)}
</React.Fragment>
)
@@ -1288,18 +1333,20 @@ export default function StoragePage() {
<p className="text-xs text-white/70">Guaranteed space reserved for this dataset</p>
</div>
{/* Mount Point */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-white">Mount Point</label>
<input
type="text"
value={datasetForm.mount_point}
onChange={(e) => setDatasetForm({ ...datasetForm, mount_point: e.target.value })}
className="w-full bg-[#233648] border border-border-dark rounded-lg px-4 py-2.5 text-sm text-white placeholder-white/50 focus:ring-2 focus:ring-primary focus:border-transparent outline-none"
placeholder="e.g., /mnt/backup-data (optional)"
/>
<p className="text-xs text-white/70">Optional mount point. Leave empty for default location.</p>
</div>
{/* Mount Point - Only for filesystem datasets */}
{datasetForm.type === 'filesystem' && (
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-white">Mount Point</label>
<input
type="text"
value={datasetForm.mount_point}
onChange={(e) => setDatasetForm({ ...datasetForm, mount_point: e.target.value })}
className="w-full bg-[#233648] border border-border-dark rounded-lg px-4 py-2.5 text-sm text-white placeholder-white/50 focus:ring-2 focus:ring-primary focus:border-transparent outline-none"
placeholder="e.g., /mnt/backup-data (optional)"
/>
<p className="text-xs text-white/70">Optional mount point. Leave empty for default location.</p>
</div>
)}
{/* Form Actions */}
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border-dark">

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
{
"folders": [
{
"path": "../../.."
}
]
}