redesign disk management UI
Some checks failed
CI / test-build (push) Has been cancelled

This commit is contained in:
2025-12-18 15:50:43 +07:00
parent 4b11d839ec
commit 78f99033fa

View File

@@ -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();
}
}