mirror of https://github.com/trapexit/mergerfs.git
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.
1142 lines
36 KiB
1142 lines
36 KiB
<!DOCTYPE html>
|
|
<html>
|
|
<head><title>mergerfs ui</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<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>
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
background-color: #1a1a1a;
|
|
color: #e0e0e0;
|
|
font-family: Arial, sans-serif;
|
|
}
|
|
.tab {
|
|
overflow: hidden;
|
|
border: 1px solid #444;
|
|
background-color: #2d2d2d;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 2px;
|
|
}
|
|
.tab button {
|
|
background-color: inherit;
|
|
border: none;
|
|
outline: none;
|
|
cursor: pointer;
|
|
padding: 14px 12px;
|
|
transition: 0.3s;
|
|
color: #e0e0e0;
|
|
flex: 1;
|
|
min-width: 120px;
|
|
white-space: nowrap;
|
|
}
|
|
.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-minfreespace-value {
|
|
flex: 0 0 55px;
|
|
border-radius: 4px 0 0 4px;
|
|
min-width: 55px;
|
|
background-color: #333;
|
|
color: #e0e0e0;
|
|
border: 1px solid #555;
|
|
border-right: none;
|
|
}
|
|
.branch-minfreespace-value:focus {
|
|
border-color: #666;
|
|
border-right: none;
|
|
}
|
|
.branch-minfreespace-unit {
|
|
flex: 0 0 55px;
|
|
border: 1px solid #555;
|
|
border-left: none;
|
|
border-radius: 0 4px 4px 0;
|
|
padding-right: 5px;
|
|
background-color: #333;
|
|
color: #e0e0e0;
|
|
text-align: center;
|
|
}
|
|
.branch-minfreespace-unit:focus {
|
|
outline: none;
|
|
border: 1px solid #666;
|
|
border-left: none;
|
|
}
|
|
.branch-path {
|
|
flex: 1;
|
|
min-width: 200px;
|
|
width: 100%;
|
|
}
|
|
.branch-mode {
|
|
width: 80px;
|
|
flex-shrink: 0;
|
|
}
|
|
.branch-controls-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-left: auto;
|
|
flex-shrink: 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
.branch-minfreespace-container {
|
|
display: flex;
|
|
gap: 0;
|
|
align-items: stretch;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background-color: transparent;
|
|
padding: 0;
|
|
flex-shrink: 0;
|
|
}
|
|
.branches-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 6px 8px;
|
|
margin-bottom: 10px;
|
|
background-color: #252525;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
font-size: 13px;
|
|
color: #e0e0e0;
|
|
}
|
|
.header-path {
|
|
flex: 1;
|
|
min-width: 200px;
|
|
}
|
|
.header-path-btn {
|
|
width: 70px;
|
|
}
|
|
.header-mode {
|
|
width: 80px;
|
|
text-align: center;
|
|
}
|
|
.header-minfreespace {
|
|
width: 120px;
|
|
text-align: left;
|
|
}
|
|
.header-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
.header-actions div {
|
|
width: 28px;
|
|
text-align: center;
|
|
}
|
|
.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: 0;
|
|
}
|
|
.submit-branches-btn:hover {
|
|
background-color: #1976d2;
|
|
}
|
|
.path-btn {
|
|
background-color: #1565c0;
|
|
color: white;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
cursor: pointer;
|
|
margin-right: 0;
|
|
flex-shrink: 0;
|
|
}
|
|
.path-btn:hover {
|
|
background-color: #1976d2;
|
|
}
|
|
.branch-move-btn {
|
|
background-color: #424242;
|
|
color: white;
|
|
border: none;
|
|
cursor: pointer;
|
|
width: 28px;
|
|
height: 28px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
flex-shrink: 0;
|
|
}
|
|
.branch-move-btn:hover {
|
|
background-color: #616161;
|
|
}
|
|
.branch-move-btn:disabled {
|
|
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;
|
|
gap: 8px;
|
|
min-height: 60px;
|
|
}
|
|
.branch-entry:hover {
|
|
background-color: #3d3d3d;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
}
|
|
.branch-entry.dragging {
|
|
cursor: grabbing;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
|
}
|
|
.branch-entry.drag-over {
|
|
border: 2px solid #1565c0;
|
|
}
|
|
.branch-path {
|
|
flex: 1;
|
|
min-width: 200px;
|
|
}
|
|
.branch-mode {
|
|
width: 80px;
|
|
flex-shrink: 0;
|
|
}
|
|
.branch-controls-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
.branch-minfreespace-container {
|
|
display: flex;
|
|
gap: 0;
|
|
align-items: stretch;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background-color: transparent;
|
|
padding: 0;
|
|
flex-shrink: 0;
|
|
}
|
|
.branch-minfreespace-value {
|
|
width: 60px;
|
|
border-radius: 4px 0 0 4px;
|
|
min-width: 60px;
|
|
background-color: #333;
|
|
color: #e0e0e0;
|
|
border: 1px solid #555;
|
|
border-right: none;
|
|
}
|
|
.branch-minfreespace-value:focus {
|
|
border-color: #666;
|
|
border-right: none;
|
|
}
|
|
.branch-minfreespace-unit {
|
|
width: 60px;
|
|
border: 1px solid #555;
|
|
border-left: none;
|
|
border-radius: 0 4px 4px 0;
|
|
padding-right: 5px;
|
|
background-color: #333;
|
|
color: #e0e0e0;
|
|
text-align: center;
|
|
min-width: 60px;
|
|
}
|
|
.branch-minfreespace-unit:focus {
|
|
outline: none;
|
|
border: 1px solid #666;
|
|
border-left: none;
|
|
}
|
|
.path-btn {
|
|
width: 70px;
|
|
background-color: #1565c0;
|
|
color: white;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
.path-btn:hover {
|
|
background-color: #1976d2;
|
|
}
|
|
.branch-move-btn {
|
|
background-color: #424242;
|
|
color: white;
|
|
border: none;
|
|
cursor: pointer;
|
|
width: 28px;
|
|
height: 28px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
flex-shrink: 0;
|
|
}
|
|
.branch-move-btn:hover {
|
|
background-color: #616161;
|
|
}
|
|
.branch-move-btn:disabled {
|
|
cursor: not-allowed;
|
|
}
|
|
.branch-remove {
|
|
width: 80px;
|
|
background-color: #8b0000;
|
|
color: white;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
.branch-remove:hover {
|
|
background-color: #a00000;
|
|
}
|
|
.branch-entry:hover {
|
|
background-color: #3d3d3d;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
}
|
|
.branch-entry.dragging {
|
|
cursor: grabbing;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
|
}
|
|
.branch-entry.drag-over {
|
|
border: 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;
|
|
}
|
|
.toast {
|
|
display: none;
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%,-50%);
|
|
color: white;
|
|
padding: 12px 24px;
|
|
border-radius: 4px;
|
|
z-index: 1000;
|
|
opacity: 0;
|
|
transition: opacity 2s;
|
|
}
|
|
.toast.show_success {
|
|
display: block;
|
|
background-color: #2e7d32;
|
|
opacity: 1;
|
|
}
|
|
.toast.show_failure {
|
|
display: block;
|
|
background-color: red;
|
|
opacity: 1;
|
|
}
|
|
.container {
|
|
padding: 6px 12px;
|
|
}
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 15px;
|
|
}
|
|
.action-buttons button {
|
|
flex: 1;
|
|
min-width: 140px;
|
|
}
|
|
.table-container {
|
|
overflow-x: auto;
|
|
width: 100%;
|
|
}
|
|
.select-wrapper {
|
|
margin-bottom: 15px;
|
|
}
|
|
.select-wrapper label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
}
|
|
.select-wrapper select {
|
|
width: 100%;
|
|
}
|
|
.controls-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 15px;
|
|
gap: 15px;
|
|
}
|
|
@media (min-width: 769px) {
|
|
.select-wrapper {
|
|
display: inline-block;
|
|
}
|
|
.select-wrapper select {
|
|
width: auto;
|
|
min-width: 200px;
|
|
}
|
|
.action-buttons {
|
|
display: inline-flex;
|
|
gap: 10px;
|
|
margin-top: 20px;
|
|
}
|
|
.action-buttons button {
|
|
flex: none;
|
|
min-width: auto;
|
|
}
|
|
}
|
|
@media (max-width: 768px) {
|
|
.controls-row {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 10px;
|
|
}
|
|
.action-buttons {
|
|
flex-direction: row;
|
|
}
|
|
.tab {
|
|
gap: 1px;
|
|
}
|
|
.tab button {
|
|
padding: 12px 8px;
|
|
font-size: 14px;
|
|
}
|
|
.tabcontent {
|
|
padding: 4px 8px;
|
|
}
|
|
.branch-entry {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 8px;
|
|
}
|
|
.branch-path {
|
|
width: 100%;
|
|
min-width: auto;
|
|
}
|
|
.branch-controls-wrapper {
|
|
flex-direction: row;
|
|
gap: 8px;
|
|
width: 100%;
|
|
flex-wrap: wrap;
|
|
}
|
|
.path-btn,
|
|
.branch-mode,
|
|
.branch-remove {
|
|
flex: 1;
|
|
min-width: 60px;
|
|
}
|
|
.branch-minfreespace-container {
|
|
display: flex;
|
|
gap: 0;
|
|
margin-bottom: 0;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background-color: transparent;
|
|
padding: 0;
|
|
flex: 1;
|
|
min-width: 120px;
|
|
}
|
|
.branch-minfreespace-value {
|
|
width: 60px;
|
|
min-width: 60px;
|
|
border-radius: 4px 0 0 4px;
|
|
background-color: #333;
|
|
color: #e0e0e0;
|
|
border: 1px solid #555;
|
|
border-right: none;
|
|
}
|
|
.branch-minfreespace-value:focus {
|
|
border-color: #666;
|
|
border-right: none;
|
|
}
|
|
.branch-minfreespace-unit {
|
|
width: 60px;
|
|
min-width: 60px;
|
|
border: 1px solid #555;
|
|
border-left: none;
|
|
border-radius: 0 4px 4px 0;
|
|
background-color: #333;
|
|
color: #e0e0e0;
|
|
text-align: center;
|
|
}
|
|
.branch-minfreespace-unit:focus {
|
|
outline: none;
|
|
border: 1px solid #666;
|
|
border-left: none;
|
|
}
|
|
.branch-move-btn {
|
|
flex: 1;
|
|
min-width: 44px;
|
|
height: 44px;
|
|
}
|
|
.branch-remove {
|
|
padding: 12px;
|
|
min-height: 44px;
|
|
}
|
|
.add-branch-btn,
|
|
.submit-branches-btn {
|
|
padding: 12px 16px;
|
|
font-size: 14px;
|
|
}
|
|
input[type="text"],
|
|
select {
|
|
padding: 10px;
|
|
font-size: 16px;
|
|
min-height: 44px;
|
|
}
|
|
.modal-content {
|
|
width: 95%;
|
|
margin: 10% auto;
|
|
padding: 15px;
|
|
}
|
|
.toast {
|
|
padding: 16px 20px;
|
|
font-size: 14px;
|
|
max-width: 90%;
|
|
}
|
|
}
|
|
@media (max-width: 480px) {
|
|
.tab {
|
|
flex-direction: column;
|
|
}
|
|
.tab button {
|
|
width: 100%;
|
|
min-width: auto;
|
|
border-bottom: 1px solid #444;
|
|
}
|
|
.action-buttons button {
|
|
width: 100%;
|
|
min-width: auto;
|
|
}
|
|
.branch-move-btn {
|
|
font-size: 14px;
|
|
}
|
|
.branch-remove {
|
|
font-size: 14px;
|
|
}
|
|
th, td {
|
|
padding: 6px;
|
|
font-size: 12px;
|
|
}
|
|
}
|
|
@media (min-width: 1200px) {
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
.tab,
|
|
.tabcontent {
|
|
border-radius: 4px;
|
|
}
|
|
}
|
|
</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 class="container">
|
|
<div class="controls-row">
|
|
<div class="select-wrapper">
|
|
<label for="mount-select-branches">Select Mountpoint:</label>
|
|
<select id="mount-select-branches"></select>
|
|
</div>
|
|
<div class="action-buttons">
|
|
<button class="add-branch-btn" onclick="addBranchEntry()">+ Add Branch</button>
|
|
<button class="submit-branches-btn" onclick="submitBranches()">Submit</button>
|
|
</div>
|
|
</div>
|
|
<div id="branches-list"></div>
|
|
</div>
|
|
</div>
|
|
<div id="Advanced" class="tabcontent">
|
|
<div class="container">
|
|
<div class="select-wrapper">
|
|
<label for="mount-select-advanced">Select Mountpoint:</label>
|
|
<select id="mount-select-advanced"></select>
|
|
</div>
|
|
<div class="table-container">
|
|
<div id="kv-list"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="pathModal" class="modal">
|
|
<div class="modal-content">
|
|
<span class="close-modal" onclick="closePathModal()">×</span>
|
|
<h3>Select Path</h3>
|
|
<div id="mount-list"></div>
|
|
</div>
|
|
</div>
|
|
<div id="toast" class="toast"></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%';
|
|
function submitValue(e) {
|
|
if (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.stopImmediatePropagation();
|
|
}
|
|
const key = encodeURIComponent(k)
|
|
const mount_uri = encodeURIComponent(mount)
|
|
const postUrl = `/kvs/${key}?mount=${mount}`
|
|
fetch(postUrl, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(input.value)
|
|
}).then(response => {
|
|
if(response.ok) {
|
|
msg = `Success: ${key}=${input.value}`
|
|
showToast(msg,true);
|
|
} else {
|
|
response.json().then(body => {
|
|
msg = `msg: ${body["error"]["msg"]}<br>`
|
|
msg += `mount: ${body["error"]["mount"]}<br>`
|
|
msg += `key: ${body["error"]["key"]}<br>`
|
|
msg += `value: ${body["error"]["value"]}`
|
|
showToast(msg,false);
|
|
});
|
|
}
|
|
});
|
|
input.blur();
|
|
}
|
|
function handleEnter(e) {
|
|
if (e.key === 'Enter' || e.keyCode === 13 || e.which === 13) {
|
|
submitValue(e);
|
|
}
|
|
}
|
|
input.onkeydown = handleEnter;
|
|
input.onkeypress = handleEnter;
|
|
input.onkeyup = handleEnter;
|
|
input.addEventListener('input', function(e) {
|
|
if (e.inputType === 'insertLineBreak') {
|
|
submitValue(e);
|
|
}
|
|
});
|
|
input.addEventListener('change', function() { submitValue(); });
|
|
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 controlsWrapper = document.createElement('div');
|
|
controlsWrapper.className = 'branch-controls-wrapper';
|
|
const pathBtn = document.createElement('button');
|
|
pathBtn.className = 'path-btn';
|
|
pathBtn.textContent = 'Mounts';
|
|
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 minfreespaceContainer = document.createElement('div');
|
|
minfreespaceContainer.className = 'branch-minfreespace-container';
|
|
const minfreespaceValueInput = document.createElement('input');
|
|
minfreespaceValueInput.type = 'number';
|
|
minfreespaceValueInput.className = 'branch-minfreespace-value';
|
|
minfreespaceValueInput.placeholder = 'minfreespace';
|
|
minfreespaceValueInput.min = '0';
|
|
const minfreespaceUnitSelect = document.createElement('select');
|
|
minfreespaceUnitSelect.className = 'branch-minfreespace-unit';
|
|
const units = [
|
|
{value: 'B', label: 'B'},
|
|
{value: 'K', label: 'KiB'},
|
|
{value: 'M', label: 'MiB'},
|
|
{value: 'G', label: 'GiB'},
|
|
{value: 'T', label: 'TiB'}
|
|
];
|
|
units.forEach(u => {
|
|
const opt = document.createElement('option');
|
|
opt.value = u.value;
|
|
opt.textContent = u.label;
|
|
minfreespaceUnitSelect.appendChild(opt);
|
|
});
|
|
let minfreespaceValue = '';
|
|
let minfreespaceUnit = 'G';
|
|
if (minfreespace) {
|
|
const match = minfreespace.match(/^(\d+)([BKMG])?$/i);
|
|
if (match) {
|
|
minfreespaceValue = match[1];
|
|
if (match[2]) {
|
|
minfreespaceUnit = match[2].toUpperCase();
|
|
}
|
|
} else {
|
|
minfreespaceValue = minfreespace;
|
|
}
|
|
}
|
|
minfreespaceValueInput.value = minfreespaceValue;
|
|
minfreespaceUnitSelect.value = minfreespaceUnit;
|
|
minfreespaceContainer.appendChild(minfreespaceValueInput);
|
|
minfreespaceContainer.appendChild(minfreespaceUnitSelect);
|
|
const moveUpBtn = document.createElement('button');
|
|
moveUpBtn.className = 'branch-move-btn branch-move-up';
|
|
moveUpBtn.innerHTML = '▲';
|
|
moveUpBtn.title = 'Move up';
|
|
moveUpBtn.onclick = () => moveEntryUp(entry);
|
|
const moveDownBtn = document.createElement('button');
|
|
moveDownBtn.className = 'branch-move-btn branch-move-down';
|
|
moveDownBtn.innerHTML = '▼';
|
|
moveDownBtn.title = 'Move down';
|
|
moveDownBtn.onclick = () => moveEntryDown(entry);
|
|
const removeBtn = document.createElement('button');
|
|
removeBtn.className = 'branch-remove';
|
|
removeBtn.textContent = 'Remove';
|
|
removeBtn.onclick = () => entry.remove();
|
|
controlsWrapper.appendChild(pathBtn);
|
|
controlsWrapper.appendChild(modeSelect);
|
|
controlsWrapper.appendChild(minfreespaceContainer);
|
|
controlsWrapper.appendChild(moveUpBtn);
|
|
controlsWrapper.appendChild(moveDownBtn);
|
|
controlsWrapper.appendChild(removeBtn);
|
|
entry.appendChild(pathInput);
|
|
entry.appendChild(controlsWrapper);
|
|
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(entry, prev);
|
|
}
|
|
}
|
|
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 = '';
|
|
const currentMount = document.getElementById('mount-select-branches').value;
|
|
fetch('/mounts')
|
|
.then(r => r.json())
|
|
.then(mounts => {
|
|
mounts.forEach(m => {
|
|
if (m.path === currentMount) return;
|
|
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);
|
|
});
|
|
if (mountList.children.length === 0) {
|
|
mountList.innerHTML = '<div style="padding: 10px; color: #888;">No other mountpoints available</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 minfreespaceValueInput = entry.querySelector('.branch-minfreespace-value');
|
|
const minfreespaceUnitSelect = entry.querySelector('.branch-minfreespace-unit');
|
|
if (pathInput && pathInput.value.trim()) {
|
|
let branchStr = pathInput.value.trim();
|
|
if (modeSelect && modeSelect.value && modeSelect.value !== 'RW') {
|
|
branchStr += '=' + modeSelect.value;
|
|
} else {
|
|
branchStr += '=RW';
|
|
}
|
|
if (minfreespaceValueInput && minfreespaceValueInput.value.trim() && minfreespaceUnitSelect) {
|
|
const value = minfreespaceValueInput.value.trim();
|
|
const unit = minfreespaceUnitSelect.value;
|
|
branchStr += ',' + value + unit;
|
|
}
|
|
branches.push(branchStr);
|
|
}
|
|
});
|
|
const branchesStr = branches.join(':');
|
|
fetch(`/kvs/branches?mount=${encodeURIComponent(mount)}`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(branchesStr)
|
|
}).then(response => {
|
|
if(response.ok) {
|
|
const msg = 'branches updated'
|
|
showToast(msg,true);
|
|
loadMounts();
|
|
} else {
|
|
response.text().then(body => {
|
|
alert('Status: ' + response.status + '\nBody: ' + body);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
let toastTimer = null;
|
|
|
|
function showToast(msg,success) {
|
|
const toast = document.getElementById('toast');
|
|
if (toastTimer) clearTimeout(toastTimer);
|
|
toast.innerHTML = msg;
|
|
if(success) {
|
|
toast.classList.remove('show_failure');
|
|
toast.classList.add('show_success');
|
|
toastTimer = setTimeout(() => toast.classList.remove('show_success'), 3000);
|
|
} else {
|
|
toast.classList.remove('show_success');
|
|
toast.classList.add('show_failure')
|
|
toastTimer = setTimeout(() => toast.classList.remove('show_failure'), 5000);
|
|
}
|
|
}
|
|
|
|
window.onclick = function(event) {
|
|
const modal = document.getElementById('pathModal');
|
|
if (event.target === modal) {
|
|
closePathModal();
|
|
}
|
|
}
|
|
window.onload = () => { loadMounts(); };
|
|
</script>
|
|
</body>
|
|
</html>
|