This commit is contained in:
@@ -144,7 +144,9 @@
|
||||
// Update storage stats
|
||||
document.getElementById('pool-count').textContent = data.storage.pool_count || 0;
|
||||
document.getElementById('total-capacity').textContent = formatBytes(data.storage.total_capacity || 0);
|
||||
document.getElementById('smb-shares').textContent = (data.services.smb_shares || 0) + ' / ' + (data.services.nfs_exports || 0);
|
||||
// Display total shares (SMB + NFS)
|
||||
const totalShares = (data.services.smb_shares || 0) + (data.services.nfs_exports || 0);
|
||||
document.getElementById('smb-shares').textContent = totalShares;
|
||||
document.getElementById('iscsi-targets').textContent = data.services.iscsi_targets || 0;
|
||||
|
||||
// Update service status
|
||||
|
||||
@@ -5,18 +5,55 @@
|
||||
<h1 class="text-3xl font-bold text-white mb-2">iSCSI Targets</h1>
|
||||
<p class="text-slate-400">Manage iSCSI targets and LUNs</p>
|
||||
</div>
|
||||
<button onclick="showCreateISCSIModal()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium">
|
||||
Create Target
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs for Disk Mode and Tape Mode -->
|
||||
<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">iSCSI Targets</h2>
|
||||
<button onclick="loadISCSITargets()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
||||
<div class="flex border-b border-slate-700">
|
||||
<button onclick="switchTab('disk')" id="tab-disk" class="flex-1 px-4 py-3 text-sm font-medium text-center border-b-2 border-blue-600 text-blue-400 bg-slate-800">
|
||||
Disk Mode
|
||||
</button>
|
||||
<button onclick="switchTab('tape')" id="tab-tape" class="flex-1 px-4 py-3 text-sm font-medium text-center border-b-2 border-transparent text-slate-400 hover:text-white">
|
||||
Tape Library Passthrough
|
||||
</button>
|
||||
</div>
|
||||
<div id="iscsi-targets-list" class="p-4">
|
||||
<p class="text-slate-400 text-sm">Loading...</p>
|
||||
|
||||
<!-- Disk Mode Content -->
|
||||
<div id="content-disk" class="tab-content">
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-white">Disk Mode Targets</h2>
|
||||
<p class="text-xs text-slate-400 mt-1">For ZVOL and block devices</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="loadISCSITargets('disk')" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
||||
<button onclick="showCreateISCSIModal('disk')" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium">
|
||||
Create Disk Target
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="iscsi-targets-list-disk" class="p-4">
|
||||
<p class="text-slate-400 text-sm">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tape Mode Content -->
|
||||
<div id="content-tape" class="tab-content hidden">
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-white">Tape Library Passthrough Targets</h2>
|
||||
<p class="text-xs text-slate-400 mt-1">For tape devices (/dev/st*, /dev/nst*)</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="loadISCSITargets('tape')" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
||||
<button onclick="showCreateISCSIModal('tape')" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg text-sm font-medium">
|
||||
Create Tape Target
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="iscsi-targets-list-tape" class="p-4">
|
||||
<p class="text-slate-400 text-sm">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -24,12 +61,15 @@
|
||||
<!-- Create iSCSI Target Modal -->
|
||||
<div id="create-iscsi-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-md w-full mx-4">
|
||||
<h3 class="text-xl font-semibold text-white mb-4">Create iSCSI Target</h3>
|
||||
<h3 id="create-iscsi-modal-title" class="text-xl font-semibold text-white mb-4">Create iSCSI Target</h3>
|
||||
<form id="create-iscsi-form" onsubmit="createISCSITarget(event)" class="space-y-4">
|
||||
<input type="hidden" name="type" id="create-iscsi-type" value="disk">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-1">IQN</label>
|
||||
<input type="text" name="iqn" placeholder="iqn.2024-12.com.atlas:target1" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
||||
<p class="text-xs text-slate-400 mt-1">iSCSI Qualified Name</p>
|
||||
<input type="text" name="iqn" id="iqn-input" placeholder="iqn.2025-12.com.atlas:target-1" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
||||
<p class="text-xs text-slate-400 mt-1">iSCSI Qualified Name (format: iqn.YYYY-MM.domain.subdomain:identifier)</p>
|
||||
<p class="text-xs text-red-400 mt-1">⚠️ Important: Domain must have at least 2 levels (e.g., com.atlas, org.example)</p>
|
||||
<p class="text-xs text-slate-500 mt-1">Example: iqn.2025-12.com.atlas:target-1 (correct) | iqn.2025-12.atlas:target-1 (wrong - domain too short)</p>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button type="button" onclick="closeModal('create-iscsi-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
||||
@@ -99,24 +139,56 @@ function formatBytes(bytes) {
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
async function loadISCSITargets() {
|
||||
// Tab switching
|
||||
let currentTab = 'disk';
|
||||
function switchTab(tab) {
|
||||
currentTab = tab;
|
||||
|
||||
// Update tab buttons
|
||||
document.getElementById('tab-disk').classList.toggle('border-blue-600', tab === 'disk');
|
||||
document.getElementById('tab-disk').classList.toggle('text-blue-400', tab === 'disk');
|
||||
document.getElementById('tab-disk').classList.toggle('border-transparent', tab !== 'disk');
|
||||
document.getElementById('tab-disk').classList.toggle('text-slate-400', tab !== 'disk');
|
||||
|
||||
document.getElementById('tab-tape').classList.toggle('border-green-600', tab === 'tape');
|
||||
document.getElementById('tab-tape').classList.toggle('text-green-400', tab === 'tape');
|
||||
document.getElementById('tab-tape').classList.toggle('border-transparent', tab !== 'tape');
|
||||
document.getElementById('tab-tape').classList.toggle('text-slate-400', tab !== 'tape');
|
||||
|
||||
// Update content visibility
|
||||
document.getElementById('content-disk').classList.toggle('hidden', tab !== 'disk');
|
||||
document.getElementById('content-tape').classList.toggle('hidden', tab !== 'tape');
|
||||
|
||||
// Load targets for active tab
|
||||
loadISCSITargets(tab);
|
||||
}
|
||||
|
||||
async function loadISCSITargets(type = 'disk') {
|
||||
try {
|
||||
const res = await fetch('/api/v1/iscsi/targets', { headers: getAuthHeaders() });
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
||||
document.getElementById('iscsi-targets-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.error || 'Failed to load iSCSI targets'}</p>`;
|
||||
const listEl = document.getElementById(`iscsi-targets-list-${type}`);
|
||||
if (listEl) {
|
||||
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${err.error || 'Failed to load iSCSI targets'}</p>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const targets = await res.json();
|
||||
const listEl = document.getElementById('iscsi-targets-list');
|
||||
const allTargets = await res.json();
|
||||
|
||||
if (!Array.isArray(targets)) {
|
||||
// Filter targets by type
|
||||
const targets = Array.isArray(allTargets) ? allTargets.filter(t => (t.type || 'disk') === type) : [];
|
||||
const listEl = document.getElementById(`iscsi-targets-list-${type}`);
|
||||
|
||||
if (!listEl) return;
|
||||
|
||||
if (!Array.isArray(allTargets)) {
|
||||
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (targets.length === 0) {
|
||||
listEl.innerHTML = '<p class="text-slate-400 text-sm">No iSCSI targets found</p>';
|
||||
listEl.innerHTML = `<p class="text-slate-400 text-sm">No ${type === 'disk' ? 'disk mode' : 'tape passthrough'} targets found</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,7 +227,7 @@ async function loadISCSITargets() {
|
||||
` : '<p class="text-sm text-slate-500 mt-2">No LUNs attached. Click "Add LUN" to bind a volume.</p>'}
|
||||
</div>
|
||||
<div class="flex gap-2 ml-4">
|
||||
<button onclick="showAddLUNModal('${target.id}')" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
||||
<button onclick="showAddLUNModal('${target.id}', '${target.type || 'disk'}')" class="px-3 py-1.5 ${target.type === 'tape' ? 'bg-green-600 hover:bg-green-700' : 'bg-blue-600 hover:bg-blue-700'} text-white rounded text-sm">
|
||||
Add LUN
|
||||
</button>
|
||||
<button onclick="showConnectionInstructions('${target.id}')" class="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
||||
@@ -173,7 +245,39 @@ async function loadISCSITargets() {
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateISCSIModal() {
|
||||
function showCreateISCSIModal(type = 'disk') {
|
||||
// Generate default IQN with current date
|
||||
// IQN format: iqn.YYYY-MM.domain.subdomain:identifier
|
||||
// Domain must have at least 2 levels (e.g., com.atlas)
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const suffix = type === 'tape' ? 'tape' : 'target';
|
||||
const defaultIQN = `iqn.${year}-${month}.com.atlas:${suffix}-1`;
|
||||
|
||||
// Set target type
|
||||
document.getElementById('create-iscsi-type').value = type;
|
||||
document.getElementById('create-iscsi-modal-title').textContent = type === 'tape' ? 'Create Tape Library Passthrough Target' : 'Create Disk Mode Target';
|
||||
|
||||
const iqnInput = document.getElementById('iqn-input');
|
||||
if (iqnInput) {
|
||||
iqnInput.value = defaultIQN;
|
||||
// Auto-fix common mistake: replace last dot with colon if needed
|
||||
iqnInput.addEventListener('blur', function() {
|
||||
let value = this.value.trim();
|
||||
// If user typed dot before identifier, replace with colon
|
||||
// Pattern: iqn.YYYY-MM.domain.identifier -> iqn.YYYY-MM.domain:identifier
|
||||
if (value.match(/^iqn\.\d{4}-\d{2}\.[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z0-9]/) && !value.includes(':')) {
|
||||
// Replace last dot with colon
|
||||
const lastDotIndex = value.lastIndexOf('.');
|
||||
if (lastDotIndex > 0) {
|
||||
value = value.substring(0, lastDotIndex) + ':' + value.substring(lastDotIndex + 1);
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('create-iscsi-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
@@ -271,24 +375,35 @@ function closeModal(modalId) {
|
||||
async function createISCSITarget(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const targetType = formData.get('type') || 'disk';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/v1/iscsi/targets', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
iqn: formData.get('iqn')
|
||||
iqn: formData.get('iqn'),
|
||||
type: targetType
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
closeModal('create-iscsi-modal');
|
||||
e.target.reset();
|
||||
loadISCSITargets();
|
||||
alert('iSCSI target created successfully');
|
||||
loadISCSITargets(targetType);
|
||||
alert(`${targetType === 'tape' ? 'Tape' : 'Disk'} target created successfully`);
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert(`Error: ${err.error || 'Failed to create iSCSI target'}`);
|
||||
const err = await res.json().catch(() => ({ error: 'Failed to parse error response' }));
|
||||
let errMsg = 'Failed to create iSCSI target';
|
||||
if (err) {
|
||||
if (err.message) {
|
||||
errMsg = err.message;
|
||||
if (err.details) errMsg += ': ' + err.details;
|
||||
} else if (err.error) {
|
||||
errMsg = err.error;
|
||||
}
|
||||
}
|
||||
alert(`Error: ${errMsg}`);
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
@@ -299,24 +414,41 @@ async function addLUN(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const targetId = formData.get('target_id');
|
||||
const targetType = formData.get('target_type') || 'disk';
|
||||
|
||||
// Get volume from either dropdown or manual input
|
||||
const zvolSelect = document.getElementById('lun-zvol-select').value;
|
||||
const zvolManual = document.getElementById('lun-zvol-manual').value.trim();
|
||||
const zvol = zvolSelect || zvolManual;
|
||||
let requestBody = {};
|
||||
|
||||
if (!zvol) {
|
||||
alert('Please select or enter a volume name');
|
||||
return;
|
||||
if (targetType === 'tape') {
|
||||
// Tape mode: use device
|
||||
const device = document.getElementById('lun-device-input').value.trim();
|
||||
if (!device) {
|
||||
alert('Please enter a tape device path');
|
||||
return;
|
||||
}
|
||||
requestBody = {
|
||||
device: device,
|
||||
backstore: 'pscsi'
|
||||
};
|
||||
} else {
|
||||
// Disk mode: use ZVOL
|
||||
const zvolSelect = document.getElementById('lun-zvol-select').value;
|
||||
const zvolManual = document.getElementById('lun-zvol-manual').value.trim();
|
||||
const zvol = zvolSelect || zvolManual;
|
||||
|
||||
if (!zvol) {
|
||||
alert('Please select or enter a volume name');
|
||||
return;
|
||||
}
|
||||
requestBody = {
|
||||
zvol: zvol
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/v1/iscsi/targets/${targetId}/luns`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({
|
||||
zvol: zvol
|
||||
})
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
@@ -324,7 +456,9 @@ async function addLUN(e) {
|
||||
e.target.reset();
|
||||
document.getElementById('lun-zvol-select').innerHTML = '<option value="">Loading volumes...</option>';
|
||||
document.getElementById('lun-zvol-manual').value = '';
|
||||
loadISCSITargets();
|
||||
document.getElementById('lun-device-input').value = '';
|
||||
// Reload targets for the current tab
|
||||
loadISCSITargets(targetType);
|
||||
alert('LUN added successfully');
|
||||
} else {
|
||||
const err = await res.json();
|
||||
@@ -348,7 +482,9 @@ async function removeLUN(targetId, lunId) {
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
loadISCSITargets();
|
||||
// Reload targets for current tab
|
||||
const activeTab = document.getElementById('content-disk').classList.contains('hidden') ? 'tape' : 'disk';
|
||||
loadISCSITargets(activeTab);
|
||||
alert('LUN removed successfully');
|
||||
} else {
|
||||
const err = await res.json();
|
||||
@@ -369,7 +505,9 @@ async function deleteISCSITarget(id) {
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
loadISCSITargets();
|
||||
// Reload targets for current tab
|
||||
const activeTab = document.getElementById('content-disk').classList.contains('hidden') ? 'tape' : 'disk';
|
||||
loadISCSITargets(activeTab);
|
||||
alert('iSCSI target deleted successfully');
|
||||
} else {
|
||||
const err = await res.json();
|
||||
@@ -380,8 +518,7 @@ async function deleteISCSITarget(id) {
|
||||
}
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
loadISCSITargets();
|
||||
// Load initial data - will be called in auth check function
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
|
||||
@@ -233,6 +233,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Spare Disk Modal -->
|
||||
<div id="add-spare-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-slate-800 rounded-lg border border-slate-700 p-4 sm:p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||
<h3 class="text-xl font-semibold text-white mb-4">Add Spare Disk</h3>
|
||||
<form id="add-spare-form" onsubmit="addSpareDisk(event)" class="space-y-4">
|
||||
<input type="hidden" id="add-spare-pool-name" name="pool">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-1">Pool Name</label>
|
||||
<input type="text" id="add-spare-pool-name-display" readonly class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm opacity-75 cursor-not-allowed">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-1">Select Disk</label>
|
||||
<select id="add-spare-disk" name="disk" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
||||
<option value="">Loading disks...</option>
|
||||
</select>
|
||||
<p class="text-xs text-slate-400 mt-1">Only available disks are shown</p>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button type="button" onclick="closeModal('add-spare-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded text-sm">
|
||||
Add Spare
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Dataset Modal -->
|
||||
<div id="edit-dataset-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-slate-800 rounded-lg border border-slate-700 p-4 sm:p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||
@@ -435,8 +464,10 @@ async function loadPools() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Rendering pools list...');
|
||||
listEl.innerHTML = pools.map(pool => `
|
||||
console.log('Rendering pools list...', pools);
|
||||
const html = pools.map(pool => {
|
||||
console.log('Rendering pool:', pool.name);
|
||||
return `
|
||||
<div class="border-b border-slate-700 last:border-0 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
@@ -462,8 +493,22 @@ async function loadPools() {
|
||||
<span class="text-white ml-2">${formatBytes(pool.free)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button onclick="togglePoolInfo('${pool.name}')" class="text-sm text-blue-400 hover:text-blue-300 flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
Pool Info
|
||||
</button>
|
||||
</div>
|
||||
<div id="pool-info-${pool.name}" class="hidden mt-4 p-4 bg-slate-900 rounded border border-slate-700">
|
||||
<p class="text-slate-400 text-sm">Loading pool details...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="showAddSpareModal('${pool.name}')" class="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded text-sm">
|
||||
Add Spare
|
||||
</button>
|
||||
<button onclick="startScrub('${pool.name}')" class="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
||||
Scrub
|
||||
</button>
|
||||
@@ -476,7 +521,14 @@ async function loadPools() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
console.log('Generated HTML contains Pool Info:', html.includes('Pool Info'));
|
||||
console.log('Generated HTML contains togglePoolInfo:', html.includes('togglePoolInfo'));
|
||||
console.log('Generated HTML length:', html.length);
|
||||
|
||||
listEl.innerHTML = html;
|
||||
console.log('Pools list rendered successfully');
|
||||
} catch (err) {
|
||||
console.error('Error in loadPools:', err);
|
||||
@@ -1069,6 +1121,197 @@ async function createZVOL(e) {
|
||||
}
|
||||
}
|
||||
|
||||
async function togglePoolInfo(poolName) {
|
||||
const infoEl = document.getElementById(`pool-info-${poolName}`);
|
||||
if (!infoEl) return;
|
||||
|
||||
if (infoEl.classList.contains('hidden')) {
|
||||
infoEl.classList.remove('hidden');
|
||||
await loadPoolInfo(poolName);
|
||||
} else {
|
||||
infoEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPoolInfo(poolName) {
|
||||
const infoEl = document.getElementById(`pool-info-${poolName}`);
|
||||
if (!infoEl) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/v1/pools/${poolName}?detail=true`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
infoEl.innerHTML = '<p class="text-red-400 text-sm">Failed to load pool details</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const detail = await res.json();
|
||||
|
||||
let html = '<div class="space-y-4">';
|
||||
|
||||
// State and Status
|
||||
html += `<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-slate-400">State:</span>
|
||||
<span class="text-white ml-2 px-2 py-1 rounded text-xs font-medium ${
|
||||
detail.state === 'ONLINE' ? 'bg-green-900 text-green-300' :
|
||||
detail.state === 'DEGRADED' ? 'bg-yellow-900 text-yellow-300' :
|
||||
'bg-red-900 text-red-300'
|
||||
}">${detail.state || 'N/A'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-slate-400">Errors:</span>
|
||||
<span class="text-white ml-2">${detail.errors || 'No known data errors'}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Scrub Info
|
||||
if (detail.scrub_info) {
|
||||
html += `<div class="text-sm">
|
||||
<span class="text-slate-400">Scrub:</span>
|
||||
<span class="text-white ml-2">${detail.scrub_info}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// VDEVs
|
||||
if (detail.vdevs && detail.vdevs.length > 0) {
|
||||
html += '<div><h4 class="text-sm font-semibold text-white mb-2">Virtual Devices:</h4>';
|
||||
html += '<div class="space-y-3">';
|
||||
detail.vdevs.forEach(vdev => {
|
||||
html += `<div class="pl-4 border-l-2 border-slate-600">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-sm font-medium text-slate-300">${vdev.name || vdev.type || 'VDEV'}</span>
|
||||
<span class="px-2 py-0.5 rounded text-xs ${
|
||||
vdev.state === 'ONLINE' ? 'bg-green-900 text-green-300' :
|
||||
vdev.state === 'DEGRADED' ? 'bg-yellow-900 text-yellow-300' :
|
||||
'bg-red-900 text-red-300'
|
||||
}">${vdev.state}</span>
|
||||
${vdev.read > 0 || vdev.write > 0 || vdev.checksum > 0 ? `
|
||||
<span class="text-xs text-slate-400">
|
||||
(R:${vdev.read} W:${vdev.write} C:${vdev.checksum})
|
||||
</span>
|
||||
` : ''}
|
||||
</div>`;
|
||||
if (vdev.disks && vdev.disks.length > 0) {
|
||||
html += '<div class="ml-4 space-y-1">';
|
||||
vdev.disks.forEach(disk => {
|
||||
html += `<div class="text-xs flex items-center gap-2">
|
||||
<span class="text-slate-300">${disk.name}</span>
|
||||
<span class="px-1.5 py-0.5 rounded text-xs ${
|
||||
disk.state === 'ONLINE' ? 'bg-green-900 text-green-300' :
|
||||
disk.state === 'DEGRADED' ? 'bg-yellow-900 text-yellow-300' :
|
||||
'bg-red-900 text-red-300'
|
||||
}">${disk.state}</span>
|
||||
${disk.read > 0 || disk.write > 0 || disk.checksum > 0 ? `
|
||||
<span class="text-slate-500">
|
||||
(R:${disk.read} W:${disk.write} C:${disk.checksum})
|
||||
</span>
|
||||
` : ''}
|
||||
</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
// Spares
|
||||
if (detail.spares && detail.spares.length > 0) {
|
||||
html += '<div><h4 class="text-sm font-semibold text-white mb-2">Spare Disks:</h4>';
|
||||
html += '<div class="flex flex-wrap gap-2">';
|
||||
detail.spares.forEach(spare => {
|
||||
html += `<span class="px-2 py-1 bg-slate-700 text-slate-300 rounded text-xs">${spare}</span>`;
|
||||
});
|
||||
html += '</div></div>';
|
||||
} else {
|
||||
html += '<div><h4 class="text-sm font-semibold text-white mb-2">Spare Disks:</h4>';
|
||||
html += '<p class="text-slate-400 text-sm">No spare disks configured</p></div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
infoEl.innerHTML = html;
|
||||
} catch (err) {
|
||||
console.error('Error loading pool info:', err);
|
||||
infoEl.innerHTML = '<p class="text-red-400 text-sm">Error loading pool details</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function showAddSpareModal(poolName) {
|
||||
document.getElementById('add-spare-pool-name').value = poolName;
|
||||
document.getElementById('add-spare-pool-name-display').value = poolName;
|
||||
document.getElementById('add-spare-modal').classList.remove('hidden');
|
||||
loadDisksForSpare();
|
||||
}
|
||||
|
||||
async function loadDisksForSpare() {
|
||||
try {
|
||||
const res = await fetch('/api/v1/disks', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (!res.ok) return;
|
||||
|
||||
const disks = await res.json();
|
||||
const selectEl = document.getElementById('add-spare-disk');
|
||||
selectEl.innerHTML = '<option value="">Select a disk...</option>';
|
||||
|
||||
disks.forEach(disk => {
|
||||
if (disk.status === 'available') {
|
||||
const option = document.createElement('option');
|
||||
option.value = disk.name;
|
||||
option.textContent = `${disk.name} (${disk.size || 'Unknown'})`;
|
||||
selectEl.appendChild(option);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error loading disks:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function addSpareDisk(event) {
|
||||
event.preventDefault();
|
||||
const poolName = document.getElementById('add-spare-pool-name').value;
|
||||
const diskName = document.getElementById('add-spare-disk').value;
|
||||
|
||||
if (!diskName) {
|
||||
alert('Please select a disk');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/v1/pools/${poolName}/spare`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ disk: diskName })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
closeModal('add-spare-modal');
|
||||
loadPools();
|
||||
alert('Spare disk added successfully');
|
||||
} else {
|
||||
const data = await res.json();
|
||||
let errMsg = 'Failed to add spare disk';
|
||||
if (data) {
|
||||
if (data.message) {
|
||||
errMsg = data.message;
|
||||
if (data.details) errMsg += ': ' + data.details;
|
||||
} else if (data.error) {
|
||||
errMsg = data.error;
|
||||
}
|
||||
}
|
||||
alert(`Error: ${errMsg}`);
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function deletePool(name) {
|
||||
if (!confirm(`Are you sure you want to delete pool "${name}"? This will destroy all data!`)) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user