Files
atlas/web/templates/shares.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}}