fix storage management and nfs
This commit is contained in:
@@ -246,6 +246,8 @@ async function loadNFSExports() {
|
||||
<div class="text-sm text-slate-400 space-y-1">
|
||||
<p>Dataset: ${exp.dataset || 'N/A'}</p>
|
||||
<p>Clients: ${exp.clients && exp.clients.length > 0 ? exp.clients.join(', ') : '*'}</p>
|
||||
<p>Root Squash: ${exp.root_squash ? '<span class="text-yellow-400">Enabled</span>' : '<span class="text-green-400">Disabled</span>'}</p>
|
||||
${exp.read_only ? '<p>Read-only: <span class="text-yellow-400">Yes</span></p>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -329,8 +331,23 @@ async function createNFSExport(e) {
|
||||
loadNFSExports();
|
||||
alert('NFS export created successfully');
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert(`Error: ${err.error || 'Failed to create NFS export'}`);
|
||||
const data = await res.json();
|
||||
let errMsg = 'Failed to create NFS export';
|
||||
if (data) {
|
||||
if (data.message) {
|
||||
errMsg = data.message;
|
||||
if (data.details) {
|
||||
errMsg += ': ' + data.details;
|
||||
}
|
||||
} else if (data.error) {
|
||||
errMsg = data.error;
|
||||
if (data.details) {
|
||||
errMsg += ': ' + data.details;
|
||||
}
|
||||
}
|
||||
}
|
||||
alert(`Error: ${errMsg}\n\nNote: The export list has been refreshed. Please check if the export was created.`);
|
||||
loadNFSExports(); // Refresh list to show current state
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
@@ -371,8 +388,23 @@ async function deleteNFSExport(id) {
|
||||
loadNFSExports();
|
||||
alert('NFS export deleted successfully');
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert(`Error: ${err.error || 'Failed to delete NFS export'}`);
|
||||
const data = await res.json();
|
||||
let errMsg = 'Failed to delete NFS export';
|
||||
if (data) {
|
||||
if (data.message) {
|
||||
errMsg = data.message;
|
||||
if (data.details) {
|
||||
errMsg += ': ' + data.details;
|
||||
}
|
||||
} else if (data.error) {
|
||||
errMsg = data.error;
|
||||
if (data.details) {
|
||||
errMsg += ': ' + data.details;
|
||||
}
|
||||
}
|
||||
}
|
||||
alert(`Error: ${errMsg}`);
|
||||
loadNFSExports(); // Refresh list to show current state
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
|
||||
@@ -233,6 +233,45 @@
|
||||
</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">
|
||||
<h3 class="text-xl font-semibold text-white mb-4">Edit Dataset</h3>
|
||||
<form id="edit-dataset-form" onsubmit="updateDataset(event)" class="space-y-4">
|
||||
<input type="hidden" id="edit-dataset-name" name="name">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-1">Dataset Name</label>
|
||||
<input type="text" id="edit-dataset-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">Quota (optional)</label>
|
||||
<input type="text" id="edit-dataset-quota" name="quota" placeholder="10G, 1T, or 'none' to remove" 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">Leave empty to keep current quota. Use 'none' to remove quota.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-1">Compression (optional)</label>
|
||||
<select id="edit-dataset-compression" name="compression" 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="">Keep current</option>
|
||||
<option value="off">off</option>
|
||||
<option value="lz4">lz4</option>
|
||||
<option value="zstd">zstd</option>
|
||||
<option value="gzip">gzip</option>
|
||||
<option value="gzip-1">gzip-1</option>
|
||||
<option value="gzip-9">gzip-9</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button type="button" onclick="closeModal('edit-dataset-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-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Storage Volume Modal -->
|
||||
<div id="create-zvol-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">
|
||||
@@ -316,16 +355,38 @@ function getAuthHeaders() {
|
||||
|
||||
async function loadPools() {
|
||||
try {
|
||||
// Add cache-busting to ensure fresh data
|
||||
const res = await fetch('/api/v1/pools?_=' + Date.now(), {
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
const listEl = document.getElementById('pools-list');
|
||||
if (!listEl) {
|
||||
console.error('pools-list element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add cache-busting to ensure fresh data
|
||||
const authHeaders = getAuthHeaders();
|
||||
const headers = {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
};
|
||||
// Merge auth headers
|
||||
Object.assign(headers, authHeaders);
|
||||
|
||||
const res = await fetch('/api/v1/pools?_=' + Date.now(), {
|
||||
headers: headers
|
||||
});
|
||||
|
||||
console.log('API response status:', res.status, res.statusText);
|
||||
|
||||
let data = null;
|
||||
try {
|
||||
const text = await res.text();
|
||||
console.log('API response text:', text.substring(0, 200));
|
||||
data = JSON.parse(text);
|
||||
console.log('Parsed data:', data);
|
||||
} catch (jsonErr) {
|
||||
console.error('JSON parse error:', jsonErr);
|
||||
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Failed to parse response</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle HTTP errors
|
||||
if (!res.ok) {
|
||||
@@ -366,12 +427,15 @@ async function loadPools() {
|
||||
}
|
||||
|
||||
const pools = data;
|
||||
console.log('Pools array:', pools, 'Length:', pools.length);
|
||||
|
||||
if (pools.length === 0) {
|
||||
console.log('No pools found, showing empty message');
|
||||
listEl.innerHTML = '<p class="text-slate-400 text-sm">No pools found. Create a pool to get started.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Rendering pools list...');
|
||||
listEl.innerHTML = pools.map(pool => `
|
||||
<div class="border-b border-slate-700 last:border-0 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -413,8 +477,13 @@ async function loadPools() {
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
console.log('Pools list rendered successfully');
|
||||
} catch (err) {
|
||||
document.getElementById('pools-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
|
||||
console.error('Error in loadPools:', err);
|
||||
const listEl = document.getElementById('pools-list');
|
||||
if (listEl) {
|
||||
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message || 'Failed to load pools'}</p>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,10 +514,18 @@ async function loadDatasets() {
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-white mb-1">${ds.name}</h3>
|
||||
${ds.mountpoint ? `<p class="text-sm text-slate-400">${ds.mountpoint}</p>` : ''}
|
||||
<div class="text-xs text-slate-500 mt-1">
|
||||
Used: ${formatBytes(ds.used || 0)} | Available: ${formatBytes(ds.available || 0)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="showEditDatasetModal('${ds.name}')" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
||||
Edit
|
||||
</button>
|
||||
<button onclick="deleteDataset('${ds.name}')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<button onclick="deleteDataset('${ds.name}')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
@@ -695,6 +772,32 @@ function showCreateDatasetModal() {
|
||||
document.getElementById('create-dataset-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function showEditDatasetModal(datasetName) {
|
||||
// Load dataset details first
|
||||
try {
|
||||
const res = await fetch(`/api/v1/datasets/${encodeURIComponent(datasetName)}`, { headers: getAuthHeaders() });
|
||||
if (!res.ok) {
|
||||
alert('Failed to load dataset details');
|
||||
return;
|
||||
}
|
||||
const dataset = await res.json();
|
||||
|
||||
// Populate form
|
||||
document.getElementById('edit-dataset-name').value = dataset.name;
|
||||
document.getElementById('edit-dataset-name-display').value = dataset.name;
|
||||
|
||||
// Get current quota and compression from dataset properties
|
||||
// Note: These might not be in the response, so we'll leave them empty
|
||||
document.getElementById('edit-dataset-quota').value = '';
|
||||
document.getElementById('edit-dataset-compression').value = '';
|
||||
|
||||
// Show modal
|
||||
document.getElementById('edit-dataset-modal').classList.remove('hidden');
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateZVOLModal() {
|
||||
document.getElementById('create-zvol-modal').classList.remove('hidden');
|
||||
}
|
||||
@@ -831,8 +934,64 @@ async function createDataset(e) {
|
||||
loadDatasets();
|
||||
alert('Dataset created successfully');
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert(`Error: ${err.error || 'Failed to create dataset'}`);
|
||||
const data = await res.json();
|
||||
let errMsg = 'Failed to create dataset';
|
||||
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 updateDataset(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const datasetName = formData.get('name');
|
||||
const data = {};
|
||||
|
||||
if (formData.get('quota')) {
|
||||
data.quota = formData.get('quota');
|
||||
}
|
||||
if (formData.get('compression')) {
|
||||
data.compression = formData.get('compression');
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/v1/datasets/${encodeURIComponent(datasetName)}`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
closeModal('edit-dataset-modal');
|
||||
e.target.reset();
|
||||
loadDatasets();
|
||||
alert('Dataset updated successfully');
|
||||
} else {
|
||||
const data = await res.json();
|
||||
let errMsg = 'Failed to update dataset';
|
||||
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}`);
|
||||
@@ -993,8 +1152,24 @@ async function exportPool(name) {
|
||||
}
|
||||
}
|
||||
|
||||
// Load initial data
|
||||
loadPools();
|
||||
// Load initial data when DOM is ready
|
||||
(function() {
|
||||
function initLoad() {
|
||||
console.log('Initializing loadPools...');
|
||||
try {
|
||||
loadPools();
|
||||
} catch (err) {
|
||||
console.error('Error calling loadPools:', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initLoad);
|
||||
} else {
|
||||
// DOM is already ready
|
||||
setTimeout(initLoad, 100); // Small delay to ensure DOM is fully ready
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user