This commit is contained in:
@@ -84,13 +84,63 @@
|
||||
|
||||
<!-- Disks Tab -->
|
||||
<div id="content-disks" class="tab-content hidden">
|
||||
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-white">Available Disks</h2>
|
||||
<button onclick="loadDisks()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
||||
<div class="space-y-6">
|
||||
<!-- System Overview Card -->
|
||||
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-white">System - Available Disks</h2>
|
||||
<p class="text-sm text-slate-400 mt-1" id="disks-summary">Loading disk information...</p>
|
||||
</div>
|
||||
<button onclick="loadDisks()" class="text-sm text-slate-400 hover:text-white transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<!-- Visual Disk Grid -->
|
||||
<div id="disks-visual" class="flex flex-wrap gap-4 mb-6">
|
||||
<p class="text-slate-400 text-sm">Loading...</p>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="border-t border-slate-700 pt-4">
|
||||
<h3 class="text-sm font-medium text-slate-300 mb-3">Status Legend</h3>
|
||||
<div class="flex flex-wrap gap-4 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded bg-green-500"></div>
|
||||
<span class="text-slate-400">Available</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded bg-blue-600"></div>
|
||||
<span class="text-slate-400">In Use</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded bg-slate-600"></div>
|
||||
<span class="text-slate-400">Free</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded bg-yellow-500"></div>
|
||||
<span class="text-slate-400">Warning</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded bg-red-500"></div>
|
||||
<span class="text-slate-400">Error</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="disks-list" class="p-4">
|
||||
<p class="text-slate-400 text-sm">Loading...</p>
|
||||
|
||||
<!-- Detailed Disk List -->
|
||||
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
||||
<div class="p-4 border-b border-slate-700">
|
||||
<h2 class="text-lg font-semibold text-white">Disk Details</h2>
|
||||
</div>
|
||||
<div id="disks-list" class="p-4">
|
||||
<p class="text-slate-400 text-sm">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -426,36 +476,149 @@ async function loadDisks() {
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
||||
document.getElementById('disks-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.error || 'Failed to load disks'}</p>`;
|
||||
document.getElementById('disks-visual').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.error || 'Failed to load disks'}</p>`;
|
||||
return;
|
||||
}
|
||||
const disks = await res.json();
|
||||
const listEl = document.getElementById('disks-list');
|
||||
const visualEl = document.getElementById('disks-visual');
|
||||
const summaryEl = document.getElementById('disks-summary');
|
||||
|
||||
if (!Array.isArray(disks)) {
|
||||
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format</p>';
|
||||
visualEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (disks.length === 0) {
|
||||
listEl.innerHTML = '<p class="text-slate-400 text-sm">No disks found</p>';
|
||||
visualEl.innerHTML = '<p class="text-slate-400 text-sm">No disks found</p>';
|
||||
summaryEl.textContent = 'No disks available';
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = disks.map(disk => `
|
||||
<div class="border-b border-slate-700 last:border-0 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-white mb-1">${disk.name}</h3>
|
||||
<div class="text-sm text-slate-400 space-y-1">
|
||||
${disk.size ? `<p>Size: ${disk.size}</p>` : ''}
|
||||
${disk.model ? `<p>Model: ${disk.model}</p>` : ''}
|
||||
// Update summary
|
||||
summaryEl.textContent = `Disk: ${disks.length}`;
|
||||
|
||||
// Visual disk blocks (like QNAP)
|
||||
visualEl.innerHTML = disks.map((disk, index) => {
|
||||
// Available disks = green (since ListDisks only returns available disks)
|
||||
const statusColor = 'bg-green-500';
|
||||
const statusBorder = 'border-green-400';
|
||||
|
||||
return `
|
||||
<div class="group relative">
|
||||
<div class="disk-block ${statusColor} ${statusBorder} border-2 rounded-xl p-5 min-w-[130px] cursor-pointer transition-all duration-200 hover:scale-110 hover:shadow-xl hover:shadow-green-500/30 hover:-translate-y-1"
|
||||
title="${disk.name} - ${disk.size || 'Unknown size'}\n${disk.path || `/dev/${disk.name}`}"
|
||||
onclick="selectDisk('${disk.name}')">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="w-16 h-16 rounded-xl bg-white/25 flex items-center justify-center mb-3 backdrop-blur-sm border border-white/30">
|
||||
<svg class="w-9 h-9 text-white drop-shadow-sm" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M4 3a2 2 0 100 4h12a2 2 0 100-4H4z"></path>
|
||||
<path fill-rule="evenodd" d="M3 8h14v7a2 2 0 01-2 2H5a2 2 0 01-2-2V8zm5 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-white font-bold text-lg mb-1 drop-shadow-sm">${disk.name}</div>
|
||||
<div class="text-white/95 text-sm font-semibold">${disk.size || 'N/A'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Status indicator -->
|
||||
<div class="absolute top-3 right-3">
|
||||
<div class="w-3.5 h-3.5 bg-white rounded-full shadow-md border-2 border-green-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Detailed disk list
|
||||
listEl.innerHTML = disks.map(disk => {
|
||||
const status = 'Available';
|
||||
const statusColor = 'bg-green-900 text-green-300';
|
||||
const diskPath = disk.path || `/dev/${disk.name}`;
|
||||
|
||||
return `
|
||||
<div class="border-b border-slate-700 last:border-0 py-4 hover:bg-slate-750 transition-colors" data-disk-name="${disk.name}">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4 flex-1">
|
||||
<!-- Disk Icon -->
|
||||
<div class="w-12 h-12 rounded-lg bg-green-500 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M4 3a2 2 0 100 4h12a2 2 0 100-4H4z"></path>
|
||||
<path fill-rule="evenodd" d="M3 8h14v7a2 2 0 01-2 2H5a2 2 0 01-2-2V8zm5 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Disk Info -->
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h3 class="text-lg font-semibold text-white">${disk.name}</h3>
|
||||
<span class="px-2 py-1 rounded text-xs font-medium ${statusColor}">${status}</span>
|
||||
<span class="text-xs text-slate-500 font-mono">${diskPath}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
|
||||
${disk.size ? `
|
||||
<div>
|
||||
<span class="text-slate-400">Size:</span>
|
||||
<span class="text-white ml-2 font-medium">${disk.size}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${disk.model ? `
|
||||
<div>
|
||||
<span class="text-slate-400">Model:</span>
|
||||
<span class="text-white ml-2">${disk.model}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div>
|
||||
<span class="text-slate-400">Type:</span>
|
||||
<span class="text-white ml-2">Physical Disk</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2">
|
||||
<button onclick="useDiskForPool('${diskPath}')"
|
||||
class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm transition-colors">
|
||||
Use for Pool
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} catch (err) {
|
||||
document.getElementById('disks-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
|
||||
document.getElementById('disks-visual').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function selectDisk(diskName) {
|
||||
// Scroll to disk in detailed list
|
||||
const diskElement = document.querySelector(`[data-disk-name="${diskName}"]`);
|
||||
if (diskElement) {
|
||||
diskElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
diskElement.classList.add('ring-2', 'ring-blue-500');
|
||||
setTimeout(() => {
|
||||
diskElement.classList.remove('ring-2', 'ring-blue-500');
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function useDiskForPool(diskPath) {
|
||||
// Open create pool modal and pre-fill with this disk
|
||||
showCreatePoolModal();
|
||||
const vdevsInput = document.querySelector('#create-pool-form input[name="vdevs"]');
|
||||
if (vdevsInput) {
|
||||
const currentValue = vdevsInput.value.trim();
|
||||
if (currentValue) {
|
||||
vdevsInput.value = currentValue + ',' + diskPath;
|
||||
} else {
|
||||
vdevsInput.value = diskPath;
|
||||
}
|
||||
vdevsInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user