You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

614 lines
21 KiB

<!DOCTYPE html>
<html>
<head><title>mergerfs ui</title>
<link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACf0lEQVQ4EY3BW0jTYRgH4N/7fv9tmkYlBBFRBCW1ESGSdeGFYZIT7Ihi6rSkoNNFUkaHmwVRiRFZBCnSAUlpohZmdiExoiBUgsSiQkQiEknISplu/+97szFjBUbPQ/D72ZPgzbENT7w/s+EF/kNZR1YmKTPvbs+zblpb3XODoA4JkZDRV9+e2nAc/1DekX0ZoEqwQMTUMQn5IOY2iW4Aq2Pu6p7TmENZ25YqaKoUMfdEzB0wlzCIEgS6/22o8xBEtwrUeffF3gr8pbg1xwfwRQPz5ON8UyGE1wS4GBBE+f0m/OmLj0SCYLrpqe7dhpg9gexcBWoATC++jxUGNwdtxDDiDF7Pmw5P2TshMmAEzZ5LLzMLAlszFDsDAIYiDju/sax/EnEYswoCylPTVzz4+XWIWPJAMuJKGH+gxPlIgG8adu5IMsbLO7JK0uvSHYgiMGJS05atFqFG98q0+jcnM0ZTEvv2rVjcsghQymb2Nu1++nH5D64XqMZVSxPd+EUEFmJEE0EZEpjy9Ve6wgtSujOIdUgTbW/Z1TlQ1Oq9pLW9VykDpQ3BiShGPCIwyX0i3h8KL3NP6QVFLYXtz/ObfZXaJFUJ6CFmRBBDACMeAaz4GsMUhKcWbnlcWvso+9aJUlun1EzrpDptpi8QkTgwi2AhDgkAAl5VetswI6vBnwtwgy2h9q53i46WrBtOB4QwSwAmxCP8QVwbBdxkfZ0ohd9vEEWIZ4lAYEBOFSGBA4jgt+CB0+cQhy0SAolhiyzDBCKxCHgPZR02cOwgkYiIDGEO9iSGVTIiLnbWArQEQh9Y23IQECLIGoE+PHB20yjm0FwcHNPaHIGhVBiIRPTBn4WjGt9/EpzPAAAAAElFTkSuQmCC">
<style>
body {
background-color: #1a1a1a;
color: #e0e0e0;
font-family: Arial, sans-serif;
}
.tab {
overflow: hidden;
border: 1px solid #444;
background-color: #2d2d2d;
}
.tab button {
background-color: inherit;
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
color: #e0e0e0;
}
.tab button:hover {
background-color: #444;
}
.tab button.active {
background-color: #3d3d3d;
}
.tabcontent {
display: none;
padding: 6px 12px;
border: 1px solid #444;
border-top: none;
background-color: #1e1e1e;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #444;
padding: 8px;
text-align: left;
}
th {
background-color: #2d2d2d;
color: #e0e0e0;
}
tr:nth-child(even) {
background-color: #252525;
}
tr:hover {
background-color: #333;
}
input[type="text"] {
background-color: #333;
color: #e0e0e0;
border: 1px solid #555;
padding: 5px;
}
input[type="text"]:focus {
outline: none;
border-color: #666;
}
select {
background-color: #333;
color: #e0e0e0;
border: 1px solid #555;
padding: 5px;
}
select:focus {
outline: none;
border-color: #666;
}
label {
color: #e0e0e0;
}
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.7);
}
.modal-content {
background-color: #2d2d2d;
margin: 15% auto;
padding: 20px;
border: 1px solid #555;
width: 80%;
max-width: 500px;
color: #e0e0e0;
}
.close-modal {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close-modal:hover,
.close-modal:focus {
color: #fff;
text-decoration: none;
cursor: pointer;
}
.branch-entry {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
padding: 10px;
border: 1px solid #444;
background-color: #252525;
}
.branch-entry input,
.branch-entry select {
padding: 5px;
background-color: #333;
color: #e0e0e0;
border: 1px solid #555;
}
.branch-entry input:focus,
.branch-entry select:focus {
outline: none;
border-color: #666;
}
.branch-path {
flex: 1;
min-width: 200px;
}
.branch-mode {
width: 80px;
}
.branch-minfreespace {
width: 100px;
}
.branch-remove {
background-color: #8b0000;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
}
.branch-remove:hover {
background-color: #a00000;
}
.add-branch-btn {
background-color: #2e7d32;
color: white;
border: none;
padding: 10px 20px;
cursor: pointer;
margin-bottom: 15px;
}
.add-branch-btn:hover {
background-color: #388e3c;
}
.submit-branches-btn {
background-color: #1565c0;
color: white;
border: none;
padding: 10px 20px;
cursor: pointer;
margin-bottom: 15px;
margin-left: 10px;
}
.submit-branches-btn:hover {
background-color: #1976d2;
}
.path-btn {
background-color: #1565c0;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
margin-right: 5px;
}
.path-btn:hover {
background-color: #1976d2;
}
.branch-move-btn {
background-color: #424242;
color: white;
border: none;
padding: 5px 8px;
cursor: pointer;
margin-right: 3px;
font-size: 12px;
}
.branch-move-btn:hover {
background-color: #616161;
}
.branch-move-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.branch-entry {
display: flex;
align-items: center;
padding: 8px;
margin: 5px 0;
background-color: #2d2d2d;
border-radius: 4px;
cursor: grab;
transition: background-color 0.2s, box-shadow 0.2s;
}
.branch-entry:hover {
background-color: #3d3d3d;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.branch-entry.dragging {
opacity: 0.5;
cursor: grabbing;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
}
.branch-entry.drag-over {
border-top: 2px solid #1565c0;
}
.mount-list-item {
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #444;
color: #e0e0e0;
}
.mount-list-item:hover {
background-color: #444;
}
h3 {
color: #e0e0e0;
}
</style>
</head>
<body>
<div class="tab">
<button class="tablinks active" onclick="openTab(event, 'Branches')">Branches</button>
<button class="tablinks" onclick="openTab(event, 'Advanced')">Advanced</button>
</div>
<div id="Branches" class="tabcontent" style="display: block;">
<div>
<label for="mount-select-branches">Select Mountpoint:</label>
<select id="mount-select-branches"></select>
</div>
<div style="margin-top: 15px;">
<button class="add-branch-btn" onclick="addBranchEntry()">+ Add Branch</button>
<button class="submit-branches-btn" onclick="submitBranches()">Submit</button>
</div>
<div id="branches-list"></div>
</div>
<div id="Advanced" class="tabcontent">
<div>
<label for="mount-select-advanced">Select Mountpoint:</label>
<select id="mount-select-advanced"></select>
</div>
<div id="kv-list"></div>
</div>
<div id="pathModal" class="modal">
<div class="modal-content">
<span class="close-modal" onclick="closePathModal()">&times;</span>
<h3>Select Path</h3>
<div id="mount-list"></div>
</div>
</div>
<script>
let g_mounts = [];
function openTab(evt, tabName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tabName).style.display = "block";
if (evt) evt.currentTarget.className += " active";
}
function populateMountSelect(selectId, onchangeFunc) {
const select = document.getElementById(selectId);
select.innerHTML = '';
g_mounts.forEach(m => {
const opt = document.createElement('option');
opt.value = m;
opt.text = m;
select.appendChild(opt);
});
select.onchange = onchangeFunc;
if (g_mounts.length > 0) {
select.value = g_mounts[0];
}
}
function getMounts() {
return g_mounts;
}
function loadMounts() {
fetch('/mounts/mergerfs')
.then(r => {
if (!r.ok) throw new Error('Failed to fetch mounts');
return r.json();
})
.then(data => {
g_mounts = data;
populateMountSelect('mount-select-advanced', function() {
loadAllForMount(this.value);
});
populateMountSelect('mount-select-branches', function() {
loadAllForMount(this.value);
});
if (g_mounts.length > 0) {
loadAllForMount(g_mounts[0]);
}
})
.catch(err => console.error('Error loading mounts:', err));
}
function fetchBranches(mount) {
return fetch('/kvs/branches?mount=' + encodeURIComponent(mount))
.then(r => {
if (!r.ok) throw new Error('Failed to fetch branches');
return r.json();
});
}
function fetchKV(mount) {
return fetch('/kvs?mount=' + encodeURIComponent(mount))
.then(r => {
if (!r.ok) throw new Error('Failed to fetch kvs');
return r.json();
});
}
function loadAllForMount(mount) {
if (!mount) return;
Promise.all([fetchKV(mount), fetchBranches(mount)])
.then(([kvData, branchesStr]) => {
renderKV(kvData, mount);
renderBranches(branchesStr);
})
.catch(err => console.error('Error loading data for mount:', mount, err));
}
function renderKV(data, mount) {
const div = document.getElementById('kv-list');
div.innerHTML = '';
const table = document.createElement('table');
const headerRow = document.createElement('tr');
const headerKey = document.createElement('th');
headerKey.textContent = 'Key';
const headerValue = document.createElement('th');
headerValue.textContent = 'Value';
headerRow.appendChild(headerKey);
headerRow.appendChild(headerValue);
table.appendChild(headerRow);
const priorityKeys = ['fsname','branches','version'];
const orderedEntries = [];
priorityKeys.forEach(key => {
if (data.hasOwnProperty(key)) {
orderedEntries.push([key, data[key]]);
delete data[key];
}
});
for (const [k, v] of Object.entries(data)) {
orderedEntries.push([k, v]);
}
for (const [k, v] of orderedEntries) {
const row = document.createElement('tr');
const keyCell = document.createElement('td');
keyCell.textContent = k;
const valueCell = document.createElement('td');
const input = document.createElement('input');
input.type = 'text';
input.value = v;
input.style.width = '100%';
input.onkeydown = function(e) {
if (e.key === 'Enter') {
const postUrl = '/kvs?mount=' + encodeURIComponent(mount);
fetch(postUrl, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({[k]: input.value})
}).then(response => {
if (!response.ok) {
response.text().then(body => {
alert('Status: ' + response.status + '\nBody: ' + body);
});
}
});
}
};
valueCell.appendChild(input);
row.appendChild(keyCell);
row.appendChild(valueCell);
table.appendChild(row);
}
div.appendChild(table);
}
function renderBranches(branchesStr) {
const div = document.getElementById('branches-list');
div.innerHTML = '';
if (!branchesStr) {
addBranchEntry();
return;
}
const branches = branchesStr.split(':').map(b => b.trim()).filter(b => b);
branches.forEach(branchStr => {
let path = branchStr;
let mode = 'RW';
let minfreespace = '';
const eqPos = branchStr.indexOf('=');
if (eqPos !== -1) {
path = branchStr.substring(0, eqPos).trim();
const options = branchStr.substring(eqPos + 1).trim();
if (options) {
const parts = options.split(',');
if (parts.length >= 1 && parts[0]) {
mode = parts[0].trim().toUpperCase();
}
if (parts.length >= 2 && parts[1]) {
minfreespace = parts[1].trim();
}
}
}
addBranchEntry(path, mode, minfreespace);
});
}
let branchEntryCounter = 0;
let pendingPathInput = null;
function addBranchEntry(path = '', mode = 'RW', minfreespace = '') {
const container = document.getElementById('branches-list');
const entry = document.createElement('div');
entry.className = 'branch-entry';
entry.draggable = true;
entry.id = 'branch-entry-' + branchEntryCounter++;
entry.addEventListener('dragstart', handleDragStart);
entry.addEventListener('dragend', handleDragEnd);
entry.addEventListener('dragover', handleDragOver);
entry.addEventListener('dragleave', handleDragLeave);
entry.addEventListener('drop', handleDrop);
const pathInput = document.createElement('input');
pathInput.type = 'text';
pathInput.className = 'branch-path';
pathInput.placeholder = 'path';
pathInput.value = path;
const pathBtn = document.createElement('button');
pathBtn.className = 'path-btn';
pathBtn.textContent = 'Path';
pathBtn.onclick = () => openPathModal(pathInput);
const modeSelect = document.createElement('select');
modeSelect.className = 'branch-mode';
['RW', 'NC', 'RO'].forEach(m => {
const opt = document.createElement('option');
opt.value = m;
opt.textContent = m;
if (m === mode) opt.selected = true;
modeSelect.appendChild(opt);
});
const minfreespaceInput = document.createElement('input');
minfreespaceInput.type = 'text';
minfreespaceInput.className = 'branch-minfreespace';
minfreespaceInput.placeholder = 'minfreespace';
minfreespaceInput.value = minfreespace;
const moveUpBtn = document.createElement('button');
moveUpBtn.className = 'branch-move-btn';
moveUpBtn.textContent = '^';
moveUpBtn.title = 'Move up';
moveUpBtn.onclick = () => moveEntryUp(entry);
const moveDownBtn = document.createElement('button');
moveDownBtn.className = 'branch-move-btn';
moveDownBtn.textContent = 'v';
moveDownBtn.title = 'Move down';
moveDownBtn.onclick = () => moveEntryDown(entry);
const removeBtn = document.createElement('button');
removeBtn.className = 'branch-remove';
removeBtn.textContent = 'Remove';
removeBtn.onclick = () => entry.remove();
entry.appendChild(pathInput);
entry.appendChild(pathBtn);
entry.appendChild(modeSelect);
entry.appendChild(minfreespaceInput);
entry.appendChild(moveUpBtn);
entry.appendChild(moveDownBtn);
entry.appendChild(removeBtn);
container.appendChild(entry);
}
let draggedEntry = null;
function handleDragStart(e) {
draggedEntry = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', this.id);
}
function handleDragEnd(e) {
this.classList.remove('dragging');
document.querySelectorAll('.branch-entry').forEach(entry => {
entry.classList.remove('drag-over');
});
draggedEntry = null;
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (this !== draggedEntry) {
this.classList.add('drag-over');
}
}
function handleDragLeave(e) {
this.classList.remove('drag-over');
}
function handleDrop(e) {
e.preventDefault();
this.classList.remove('drag-over');
if (this !== draggedEntry) {
const container = document.getElementById('branches-list');
const allEntries = Array.from(container.querySelectorAll('.branch-entry'));
const draggedIndex = allEntries.indexOf(draggedEntry);
const targetIndex = allEntries.indexOf(this);
if (draggedIndex < targetIndex) {
container.insertBefore(draggedEntry, this.nextSibling);
} else {
container.insertBefore(draggedEntry, this);
}
}
}
function moveEntryUp(entry) {
const prev = entry.previousElementSibling;
if (prev) {
entry.parentNode.insertBefore(prev, entry);
}
}
function moveEntryDown(entry) {
const next = entry.nextElementSibling;
if (next) {
entry.parentNode.insertBefore(next, entry);
}
}
function openPathModal(targetInput) {
pendingPathInput = targetInput;
const modal = document.getElementById('pathModal');
const mountList = document.getElementById('mount-list');
mountList.innerHTML = '';
fetch('/mounts')
.then(r => r.json())
.then(mounts => {
mounts.forEach(m => {
const div = document.createElement('div');
div.className = 'mount-list-item';
div.innerHTML = '<span style="color: #888;">[' + m.type + ']</span> ' + m.path;
div.onclick = () => {
if (pendingPathInput) {
pendingPathInput.value = m.path;
}
closePathModal();
};
mountList.appendChild(div);
});
})
.catch(err => {
console.error('Error loading mounts:', err);
mountList.innerHTML = '<div style="padding: 10px; color: #ff6b6b;">Error loading mounts</div>';
});
modal.style.display = 'block';
}
function closePathModal() {
document.getElementById('pathModal').style.display = 'none';
pendingPathInput = null;
}
function submitBranches() {
const mount = document.getElementById('mount-select-branches').value;
const entries = document.querySelectorAll('.branch-entry');
const branches = [];
entries.forEach(entry => {
const pathInput = entry.querySelector('.branch-path');
const modeSelect = entry.querySelector('.branch-mode');
const minfreespaceInput = entry.querySelector('.branch-minfreespace');
if (pathInput && pathInput.value.trim()) {
let branchStr = pathInput.value.trim();
if (modeSelect && modeSelect.value && modeSelect.value !== 'RW') {
branchStr += '=' + modeSelect.value;
} else {
branchStr += '=RW';
}
if (minfreespaceInput && minfreespaceInput.value.trim()) {
branchStr += ',' + minfreespaceInput.value.trim();
}
branches.push(branchStr);
}
});
const branchesStr = branches.join(':');
fetch('/kvs?mount=' + encodeURIComponent(mount), {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({"branches": branchesStr})
}).then(response => {
if (!response.ok) {
response.text().then(body => {
alert('Status: ' + response.status + '\nBody: ' + body);
});
}
});
}
window.onclick = function(event) {
const modal = document.getElementById('pathModal');
if (event.target === modal) {
closePathModal();
}
}
window.onload = () => { loadMounts(); };
</script>
</body>
</html>