380 lines
15 KiB
HTML
380 lines
15 KiB
HTML
{{define "shares-content"}}
|
|
<div class="space-y-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-white mb-2">Storage Shares</h1>
|
|
<p class="text-slate-400">Manage SMB and NFS shares</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="border-b border-slate-800">
|
|
<nav class="flex gap-4">
|
|
<button onclick="switchTab('smb')" id="tab-smb" class="tab-button px-4 py-2 border-b-2 border-blue-600 text-blue-400 font-medium">
|
|
SMB Shares
|
|
</button>
|
|
<button onclick="switchTab('nfs')" id="tab-nfs" class="tab-button px-4 py-2 border-b-2 border-transparent text-slate-400 hover:text-slate-300">
|
|
NFS Exports
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- SMB Shares Tab -->
|
|
<div id="content-smb" class="tab-content">
|
|
<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">SMB/CIFS Shares</h2>
|
|
<div class="flex gap-2">
|
|
<button onclick="showCreateSMBModal()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Create Share
|
|
</button>
|
|
<button onclick="loadSMBShares()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
|
</div>
|
|
</div>
|
|
<div id="smb-shares-list" class="p-4">
|
|
<p class="text-slate-400 text-sm">Loading...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- NFS Exports Tab -->
|
|
<div id="content-nfs" 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">NFS Exports</h2>
|
|
<div class="flex gap-2">
|
|
<button onclick="showCreateNFSModal()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Create Export
|
|
</button>
|
|
<button onclick="loadNFSExports()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
|
</div>
|
|
</div>
|
|
<div id="nfs-exports-list" class="p-4">
|
|
<p class="text-slate-400 text-sm">Loading...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create SMB Share Modal -->
|
|
<div id="create-smb-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 SMB Share</h3>
|
|
<form id="create-smb-form" onsubmit="createSMBShare(event)" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Share Name</label>
|
|
<input type="text" name="name" 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">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Dataset</label>
|
|
<input type="text" name="dataset" placeholder="pool/dataset" 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">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Description (optional)</label>
|
|
<input type="text" name="description" 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">
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<input type="checkbox" name="readonly" id="smb-readonly" class="w-4 h-4 text-blue-600 bg-slate-900 border-slate-700 rounded">
|
|
<label for="smb-readonly" class="text-sm text-slate-300">Read-only</label>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<input type="checkbox" name="guest_ok" id="smb-guest" class="w-4 h-4 text-blue-600 bg-slate-900 border-slate-700 rounded">
|
|
<label for="smb-guest" class="text-sm text-slate-300">Allow guest access</label>
|
|
</div>
|
|
<div class="flex gap-2 justify-end">
|
|
<button type="button" onclick="closeModal('create-smb-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">
|
|
Create
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create NFS Export Modal -->
|
|
<div id="create-nfs-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 NFS Export</h3>
|
|
<form id="create-nfs-form" onsubmit="createNFSExport(event)" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Dataset</label>
|
|
<input type="text" name="dataset" placeholder="pool/dataset" 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">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Clients (comma-separated)</label>
|
|
<input type="text" name="clients" placeholder="192.168.1.0/24,*" 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 or use * for all clients</p>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<input type="checkbox" name="readonly" id="nfs-readonly" class="w-4 h-4 text-blue-600 bg-slate-900 border-slate-700 rounded">
|
|
<label for="nfs-readonly" class="text-sm text-slate-300">Read-only</label>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<input type="checkbox" name="root_squash" id="nfs-rootsquash" class="w-4 h-4 text-blue-600 bg-slate-900 border-slate-700 rounded" checked>
|
|
<label for="nfs-rootsquash" class="text-sm text-slate-300">Root squash</label>
|
|
</div>
|
|
<div class="flex gap-2 justify-end">
|
|
<button type="button" onclick="closeModal('create-nfs-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">
|
|
Create
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentTab = 'smb';
|
|
|
|
function switchTab(tab) {
|
|
currentTab = tab;
|
|
document.querySelectorAll('.tab-button').forEach(btn => {
|
|
btn.classList.remove('border-blue-600', 'text-blue-400');
|
|
btn.classList.add('border-transparent', 'text-slate-400');
|
|
});
|
|
document.getElementById(`tab-${tab}`).classList.remove('border-transparent', 'text-slate-400');
|
|
document.getElementById(`tab-${tab}`).classList.add('border-blue-600', 'text-blue-400');
|
|
|
|
document.querySelectorAll('.tab-content').forEach(content => {
|
|
content.classList.add('hidden');
|
|
});
|
|
document.getElementById(`content-${tab}`).classList.remove('hidden');
|
|
|
|
if (tab === 'smb') loadSMBShares();
|
|
else if (tab === 'nfs') loadNFSExports();
|
|
}
|
|
|
|
function getAuthHeaders() {
|
|
const token = localStorage.getItem('atlas_token');
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
|
};
|
|
}
|
|
|
|
async function loadSMBShares() {
|
|
try {
|
|
const res = await fetch('/api/v1/shares/smb', { headers: getAuthHeaders() });
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
document.getElementById('smb-shares-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.error || 'Failed to load SMB shares'}</p>`;
|
|
return;
|
|
}
|
|
const shares = await res.json();
|
|
const listEl = document.getElementById('smb-shares-list');
|
|
|
|
if (!Array.isArray(shares)) {
|
|
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format</p>';
|
|
return;
|
|
}
|
|
|
|
if (shares.length === 0) {
|
|
listEl.innerHTML = '<p class="text-slate-400 text-sm">No SMB shares found</p>';
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = shares.map(share => `
|
|
<div class="border-b border-slate-700 last:border-0 py-4">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<h3 class="text-lg font-semibold text-white">${share.name}</h3>
|
|
${share.enabled ? '<span class="px-2 py-1 rounded text-xs font-medium bg-green-900 text-green-300">Enabled</span>' : '<span class="px-2 py-1 rounded text-xs font-medium bg-slate-700 text-slate-300">Disabled</span>'}
|
|
</div>
|
|
<div class="text-sm text-slate-400 space-y-1">
|
|
<p>Path: ${share.path || 'N/A'}</p>
|
|
<p>Dataset: ${share.dataset || 'N/A'}</p>
|
|
${share.description ? `<p>Description: ${share.description}</p>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button onclick="deleteSMBShare('${share.id}')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (err) {
|
|
document.getElementById('smb-shares-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
|
|
}
|
|
}
|
|
|
|
async function loadNFSExports() {
|
|
try {
|
|
const res = await fetch('/api/v1/exports/nfs', { headers: getAuthHeaders() });
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
document.getElementById('nfs-exports-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.error || 'Failed to load NFS exports'}</p>`;
|
|
return;
|
|
}
|
|
const exports = await res.json();
|
|
const listEl = document.getElementById('nfs-exports-list');
|
|
|
|
if (!Array.isArray(exports)) {
|
|
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format</p>';
|
|
return;
|
|
}
|
|
|
|
if (exports.length === 0) {
|
|
listEl.innerHTML = '<p class="text-slate-400 text-sm">No NFS exports found</p>';
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = exports.map(exp => `
|
|
<div class="border-b border-slate-700 last:border-0 py-4">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<h3 class="text-lg font-semibold text-white">${exp.path || 'N/A'}</h3>
|
|
${exp.enabled ? '<span class="px-2 py-1 rounded text-xs font-medium bg-green-900 text-green-300">Enabled</span>' : '<span class="px-2 py-1 rounded text-xs font-medium bg-slate-700 text-slate-300">Disabled</span>'}
|
|
</div>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button onclick="deleteNFSExport('${exp.id}')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (err) {
|
|
document.getElementById('nfs-exports-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
|
|
}
|
|
}
|
|
|
|
function showCreateSMBModal() {
|
|
document.getElementById('create-smb-modal').classList.remove('hidden');
|
|
}
|
|
|
|
function showCreateNFSModal() {
|
|
document.getElementById('create-nfs-modal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeModal(modalId) {
|
|
document.getElementById(modalId).classList.add('hidden');
|
|
}
|
|
|
|
async function createSMBShare(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
const data = {
|
|
name: formData.get('name'),
|
|
dataset: formData.get('dataset'),
|
|
read_only: formData.get('readonly') === 'on',
|
|
guest_ok: formData.get('guest_ok') === 'on'
|
|
};
|
|
if (formData.get('description')) data.description = formData.get('description');
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/shares/smb', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
if (res.ok) {
|
|
closeModal('create-smb-modal');
|
|
e.target.reset();
|
|
loadSMBShares();
|
|
alert('SMB share created successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to create SMB share'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function createNFSExport(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
const clients = formData.get('clients') ? formData.get('clients').split(',').map(c => c.trim()).filter(c => c) : ['*'];
|
|
const data = {
|
|
dataset: formData.get('dataset'),
|
|
clients: clients,
|
|
read_only: formData.get('readonly') === 'on',
|
|
root_squash: formData.get('root_squash') === 'on'
|
|
};
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/exports/nfs', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
if (res.ok) {
|
|
closeModal('create-nfs-modal');
|
|
e.target.reset();
|
|
loadNFSExports();
|
|
alert('NFS export created successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to create NFS export'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function deleteSMBShare(id) {
|
|
if (!confirm('Are you sure you want to delete this SMB share?')) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/shares/smb/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
if (res.ok) {
|
|
loadSMBShares();
|
|
alert('SMB share deleted successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to delete SMB share'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function deleteNFSExport(id) {
|
|
if (!confirm('Are you sure you want to delete this NFS export?')) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/exports/nfs/${id}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
if (res.ok) {
|
|
loadNFSExports();
|
|
alert('NFS export deleted successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to delete NFS export'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Load initial data
|
|
loadSMBShares();
|
|
</script>
|
|
{{end}}
|
|
|
|
{{define "shares.html"}}
|
|
{{template "base" .}}
|
|
{{end}}
|