Edit configuration in GUI; use XML configuration
This commit is contained in:
175
gui/app.js
175
gui/app.js
@@ -4,6 +4,28 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
var prevDate = 0;
|
||||
var modelGetOK = true;
|
||||
|
||||
$scope.connections = {};
|
||||
$scope.config = {};
|
||||
$scope.myID = "";
|
||||
$scope.nodes = [];
|
||||
|
||||
// Strings before bools look better
|
||||
$scope.settings = [
|
||||
{id: 'ListenAddress', descr:"Sync Protocol Listen Address", type: 'string', restart: true},
|
||||
{id: 'GUIAddress', descr: "GUI Listen Address", type: 'string', restart: true},
|
||||
{id: 'MaxSendKbps', descr: "Outgoing Rate Limit (KBps)", type: 'string', restart: true},
|
||||
{id: 'RescanIntervalS', descr: "Rescan Interval (s)", type: 'string', restart: true},
|
||||
{id: 'ReconnectIntervalS', descr: "Reconnect Interval (s)", type: 'string', restart: true},
|
||||
{id: 'ParallelRequests', descr: "Max Outstanding Requests", type: 'string', restart: true},
|
||||
{id: 'MaxChangeKbps', descr: "Max File Change Rate (KBps)", type: 'string', restart: true},
|
||||
|
||||
{id: 'ReadOnly', descr: "Read Only", type: 'bool', restart: true},
|
||||
{id: 'AllowDelete', descr: "Allow Delete", type: 'bool', restart: true},
|
||||
{id: 'FollowSymlinks', descr: "Follow Symlinks", type: 'bool', restart: true},
|
||||
{id: 'GlobalAnnEnabled', descr: "Global Announce", type: 'bool', restart: true},
|
||||
{id: 'LocalAnnEnabled', descr: "Local Announce", type: 'bool', restart: true},
|
||||
];
|
||||
|
||||
function modelGetSucceeded() {
|
||||
if (!modelGetOK) {
|
||||
$('#networkError').modal('hide');
|
||||
@@ -21,8 +43,25 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
$http.get("/rest/version").success(function (data) {
|
||||
$scope.version = data;
|
||||
});
|
||||
$http.get("/rest/config").success(function (data) {
|
||||
$scope.config = data;
|
||||
$http.get("/rest/system").success(function (data) {
|
||||
$scope.system = data;
|
||||
$scope.myID = data.myID;
|
||||
|
||||
$http.get("/rest/config").success(function (data) {
|
||||
$scope.config = data;
|
||||
|
||||
var nodes = $scope.config.Repositories[0].Nodes;
|
||||
nodes.sort(function (a, b) {
|
||||
if (a.NodeID == $scope.myID)
|
||||
return -1;
|
||||
if (b.NodeID == $scope.myID)
|
||||
return 1;
|
||||
if (a.NodeID < b.NodeID)
|
||||
return -1;
|
||||
return a.NodeID > b.NodeID;
|
||||
})
|
||||
$scope.nodes = nodes;
|
||||
});
|
||||
});
|
||||
|
||||
$scope.refresh = function () {
|
||||
@@ -70,6 +109,122 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
});
|
||||
};
|
||||
|
||||
$scope.nodeIcon = function (nodeCfg) {
|
||||
if (nodeCfg.NodeID === $scope.myID) {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
if ($scope.connections[nodeCfg.NodeID]) {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
return "minus";
|
||||
};
|
||||
|
||||
$scope.nodeClass = function (nodeCfg) {
|
||||
if (nodeCfg.NodeID === $scope.myID) {
|
||||
return "default";
|
||||
}
|
||||
|
||||
var conn = $scope.connections[nodeCfg.NodeID];
|
||||
if (conn) {
|
||||
return "success";
|
||||
}
|
||||
|
||||
return "info";
|
||||
};
|
||||
|
||||
$scope.nodeAddr = function (nodeCfg) {
|
||||
if (nodeCfg.NodeID === $scope.myID) {
|
||||
return "this node";
|
||||
}
|
||||
var conn = $scope.connections[nodeCfg.NodeID];
|
||||
if (conn) {
|
||||
return conn.Address;
|
||||
}
|
||||
return nodeCfg.Addresses.join(", ");
|
||||
};
|
||||
|
||||
$scope.nodeVer = function (nodeCfg) {
|
||||
if (nodeCfg.NodeID === $scope.myID) {
|
||||
return $scope.version;
|
||||
}
|
||||
var conn = $scope.connections[nodeCfg.NodeID];
|
||||
if (conn) {
|
||||
return conn.ClientVersion;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
$scope.saveSettings = function () {
|
||||
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
|
||||
$('#settingsTable').collapse('hide');
|
||||
};
|
||||
|
||||
$scope.editNode = function (nodeCfg) {
|
||||
$scope.currentNode = nodeCfg;
|
||||
$scope.editingExisting = true;
|
||||
$scope.currentNode.AddressesStr = nodeCfg.Addresses.join(", ")
|
||||
$('#editNode').modal({backdrop: 'static', keyboard: false});
|
||||
};
|
||||
|
||||
$scope.addNode = function () {
|
||||
$scope.currentNode = {NodeID: "", AddressesStr: "dynamic"};
|
||||
$scope.editingExisting = false;
|
||||
$('#editNode').modal({backdrop: 'static', keyboard: false});
|
||||
};
|
||||
|
||||
$scope.deleteNode = function () {
|
||||
$('#editNode').modal('hide');
|
||||
if (!$scope.editingExisting)
|
||||
return;
|
||||
|
||||
var newNodes = [];
|
||||
for (var i = 0; i < $scope.nodes.length; i++) {
|
||||
if ($scope.nodes[i].NodeID !== $scope.currentNode.NodeID) {
|
||||
newNodes.push($scope.nodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.nodes = newNodes;
|
||||
$scope.config.Repositories[0].Nodes = newNodes;
|
||||
|
||||
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}})
|
||||
}
|
||||
|
||||
$scope.saveNode = function () {
|
||||
$('#editNode').modal('hide');
|
||||
nodeCfg = $scope.currentNode;
|
||||
nodeCfg.Addresses = nodeCfg.AddressesStr.split(',').map(function (x) { return x.trim(); });
|
||||
|
||||
var done = false;
|
||||
for (var i = 0; i < $scope.nodes.length; i++) {
|
||||
if ($scope.nodes[i].NodeID === nodeCfg.NodeID) {
|
||||
$scope.nodes[i] = nodeCfg;
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!done) {
|
||||
$scope.nodes.push(nodeCfg);
|
||||
}
|
||||
|
||||
$scope.nodes.sort(function (a, b) {
|
||||
if (a.NodeID == $scope.myID)
|
||||
return -1;
|
||||
if (b.NodeID == $scope.myID)
|
||||
return 1;
|
||||
if (a.NodeID < b.NodeID)
|
||||
return -1;
|
||||
return a.NodeID > b.NodeID;
|
||||
})
|
||||
|
||||
$scope.config.Repositories[0].Nodes = $scope.nodes;
|
||||
|
||||
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}})
|
||||
};
|
||||
|
||||
$scope.refresh();
|
||||
setInterval($scope.refresh, 10000);
|
||||
});
|
||||
@@ -90,7 +245,7 @@ syncthing.filter('natural', function() {
|
||||
syncthing.filter('binary', function() {
|
||||
return function(input) {
|
||||
if (input === undefined) {
|
||||
return '- '
|
||||
return '0 '
|
||||
}
|
||||
if (input > 1024 * 1024 * 1024) {
|
||||
input /= 1024 * 1024 * 1024;
|
||||
@@ -111,7 +266,7 @@ syncthing.filter('binary', function() {
|
||||
syncthing.filter('metric', function() {
|
||||
return function(input) {
|
||||
if (input === undefined) {
|
||||
return '- '
|
||||
return '0 '
|
||||
}
|
||||
if (input > 1000 * 1000 * 1000) {
|
||||
input /= 1000 * 1000 * 1000;
|
||||
@@ -143,3 +298,15 @@ syncthing.filter('alwaysNumber', function() {
|
||||
return input;
|
||||
}
|
||||
});
|
||||
|
||||
syncthing.directive('optionEditor', function() {
|
||||
return {
|
||||
restrict: 'C',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
scope: {
|
||||
setting: '=setting',
|
||||
},
|
||||
template: '<input type="text" ng-model="config.Options[setting.id]"></input>',
|
||||
};
|
||||
})
|
||||
|
||||
7
gui/bootstrap/css/bootstrap-theme.min.css
vendored
Executable file
7
gui/bootstrap/css/bootstrap-theme.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
12
gui/bootstrap/css/bootstrap.min.css
vendored
12
gui/bootstrap/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
0
gui/bootstrap/fonts/glyphicons-halflings-regular.eot
Normal file → Executable file
0
gui/bootstrap/fonts/glyphicons-halflings-regular.eot
Normal file → Executable file
0
gui/bootstrap/fonts/glyphicons-halflings-regular.svg
Normal file → Executable file
0
gui/bootstrap/fonts/glyphicons-halflings-regular.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
0
gui/bootstrap/fonts/glyphicons-halflings-regular.ttf
Normal file → Executable file
0
gui/bootstrap/fonts/glyphicons-halflings-regular.ttf
Normal file → Executable file
0
gui/bootstrap/fonts/glyphicons-halflings-regular.woff
Normal file → Executable file
0
gui/bootstrap/fonts/glyphicons-halflings-regular.woff
Normal file → Executable file
8
gui/bootstrap/js/bootstrap.min.js
vendored
Normal file → Executable file
8
gui/bootstrap/js/bootstrap.min.js
vendored
Normal file → Executable file
File diff suppressed because one or more lines are too long
168
gui/index.html
168
gui/index.html
@@ -35,6 +35,11 @@ html, body {
|
||||
.text-monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.table-condensed>thead>tr>th, .table-condensed>tbody>tr>th, .table-condensed>tfoot>tr>th, .table-condensed>thead>tr>td, .table-condensed>tbody>tr>td, .table-condensed>tfoot>tr>td {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -63,6 +68,60 @@ html, body {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading"><h3 class="panel-title">Cluster</h3></div>
|
||||
<table class="table table-condensed">
|
||||
<tbody>
|
||||
<tr ng-repeat="nodeCfg in nodes" ng-class="{'text-muted': nodeCfg.NodeID == myID}">
|
||||
<td>
|
||||
<span class="label label-{{nodeClass(nodeCfg)}}">
|
||||
<span class="glyphicon glyphicon-{{nodeIcon(nodeCfg)}}"></span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-monospace">{{nodeCfg.NodeID | short}}</span>
|
||||
</td>
|
||||
<td>
|
||||
{{nodeVer(nodeCfg)}}
|
||||
</td>
|
||||
<td>
|
||||
{{nodeAddr(nodeCfg)}}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span ng-show="nodeCfg.NodeID != myID">
|
||||
<abbr title="{{connections[nodeCfg.NodeID].InBytesTotal | binary}}B">{{connections[nodeCfg.NodeID].inbps | metric}}bps</abbr>
|
||||
<span class="text-muted glyphicon glyphicon-chevron-down"></span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span ng-show="nodeCfg.NodeID != myID">
|
||||
<abbr title="{{connections[nodeCfg.NodeID].OutBytesTotal | binary}}B">{{connections[nodeCfg.NodeID].outbps | metric}}bps</abbr>
|
||||
<span class="text-muted glyphicon glyphicon-chevron-up"></span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button ng-show="nodeCfg.NodeID != myID" type="button" ng-click="editNode(nodeCfg)" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-pencil"></span> Edit</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right">
|
||||
<button type="button" class="btn btn-default btn-xs" ng-click="addNode()"><span class="glyphicon glyphicon-plus"></span> Add</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-info">
|
||||
@@ -75,7 +134,9 @@ html, body {
|
||||
<span class="text-muted">(+{{model.localDeleted | alwaysNumber}} delete records)</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading"><h3 class="panel-title">System</h3></div>
|
||||
<div class="panel-body">
|
||||
@@ -83,57 +144,34 @@ html, body {
|
||||
<p>{{system.cpuPercent | alwaysNumber | natural:1}}% CPU, {{system.goroutines | alwaysNumber}} goroutines</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="model.needFiles > 0">
|
||||
<h2>Files to Synchronize</h2>
|
||||
<table class="table table-condensed table-striped">
|
||||
<tr ng-repeat="file in need track by $index">
|
||||
<td><abbr title="{{file.Name}}">{{file.ShortName}}</abbr></td>
|
||||
<td class="text-right">{{file.Size | binary}}B</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading"><h3 class="panel-title">Cluster</h3></div>
|
||||
<table class="table table-condensed">
|
||||
<tbody>
|
||||
<tr ng-repeat="(node, address) in config.nodes" ng-class="{'text-primary': !!connections[node], 'text-muted': node == config.myID}">
|
||||
<td><span class="text-monospace">{{node | short}}</span></td>
|
||||
<td>
|
||||
<span ng-show="node != config.myID">{{connections[node].ClientVersion}}</span>
|
||||
<span ng-show="node == config.myID">{{version}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span ng-show="node == config.myID">
|
||||
<span class="glyphicon glyphicon-ok"></span>
|
||||
(this node)
|
||||
</span>
|
||||
<span ng-show="node != config.myID && !!connections[node]">
|
||||
<span class="glyphicon glyphicon-link"></span>
|
||||
{{connections[node].Address}}
|
||||
</span>
|
||||
<span ng-show="node != config.myID && !connections[node]">
|
||||
<span class="glyphicon glyphicon-cog"></span>
|
||||
{{address}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span ng-show="node != config.myID">
|
||||
<abbr title="{{connections[node].InBytesTotal | binary}}B">{{connections[node].inbps | metric}}b/s</abbr>
|
||||
<span class="text-muted glyphicon glyphicon-cloud-download"></span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span ng-show="node != config.myID">
|
||||
<abbr title="{{connections[node].OutBytesTotal | binary}}B">{{connections[node].outbps | metric}}b/s</abbr>
|
||||
<span class="text-muted glyphicon glyphicon-cloud-upload"></span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-heading"><h3 class="panel-title"><a href="" data-toggle="collapse" data-target="#settingsTable">Settings</a></h3></div>
|
||||
<div id="settingsTable" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<form role="form">
|
||||
<div class="form-group" ng-repeat="setting in settings">
|
||||
<div ng-if="setting.type == 'string'">
|
||||
<label for="{{setting.id}}">{{setting.descr}}</label>
|
||||
<input id="{{setting.id}}" class="form-control" type="text" ng-model="config.Options[setting.id]"></input>
|
||||
</div>
|
||||
<div class="checkbox" ng-if="setting.type == 'bool'">
|
||||
<label>
|
||||
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.Options[setting.id]"></input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="saveSettings()">Save</button>
|
||||
<small><span class="text-muted">Changes take effect when restarting syncthing.</span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,6 +204,40 @@ html, body {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editNode" class="modal fade">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Edit Node</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form">
|
||||
<div class="form-group">
|
||||
<label for="nodeID">Node ID</label>
|
||||
<input placeholder="YUFJOUDPORCMA..." ng-disabled="editingExisting" id="nodeID" class="form-control" type="text" ng-model="currentNode.NodeID"></input>
|
||||
<p class="help-block">The node ID can be found in the logs or in the "Add Node" dialog on the other node.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="addresses">Addresses</label>
|
||||
<input placeholder="dynamic" id="addresses" class="form-control" type="text" ng-model="currentNode.AddressesStr"></input>
|
||||
<p class="help-block">Enter comma separated <span class="text-monospace">ip:port</span> addresses or <span class="text-monospace">dynamic</span> to perform automatic discovery of the address.</p>
|
||||
</div>
|
||||
</form>
|
||||
<div ng-show="!editingExisting">
|
||||
When adding a new node, keep in mind that <em>this node</em> must be added on the other side too. The Node ID of this node is:
|
||||
<pre>{{myID}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="saveNode()">Save</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button ng-if="editingExisting" type="button" class="btn btn-danger pull-left" ng-click="deleteNode()">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="angular.min.js"></script>
|
||||
<script src="jquery-2.0.3.min.js"></script>
|
||||
<script src="bootstrap/js/bootstrap.min.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user