fixing storage management dashboard
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
1621
frontend/src/pages/Storage.tsx.backup
Normal file
1621
frontend/src/pages/Storage.tsx.backup
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/src/pages/calypso.code-workspace
Normal file
7
frontend/src/pages/calypso.code-workspace
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "../../.."
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user