Show FolderErrors result in UI (fixes #1437)

This commit is contained in:
Jakob Borg 2015-06-26 14:22:52 +02:00
parent 2d9fcf6828
commit 60004ebff1
6 changed files with 124 additions and 46 deletions

View File

@ -50,6 +50,7 @@
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "Error",
"External File Versioning": "External File Versioning",
"Failed Items": "Failed Items",
"File Pull Order": "File Pull Order",
"File Versioning": "File Versioning",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
@ -97,7 +98,7 @@
"OK": "OK",
"Off": "Off",
"Oldest First": "Oldest First",
"Out Of Sync": "Out Of Sync",
"Out of Sync": "Out of Sync",
"Out of Sync Items": "Out of Sync Items",
"Outgoing Rate Limit (KiB/s)": "Outgoing Rate Limit (KiB/s)",
"Override Changes": "Override Changes",
@ -163,6 +164,7 @@
"The folder ID must be unique.": "The folder ID must be unique.",
"The folder path cannot be blank.": "The folder path cannot be blank.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.",
"The following items could not be synchronized.": "The following items could not be synchronized.",
"The maximum age must be a number and cannot be blank.": "The maximum age must be a number and cannot be blank.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
@ -171,6 +173,7 @@
"The number of versions must be a number and cannot be blank.": "The number of versions must be a number and cannot be blank.",
"The path cannot be blank.": "The path cannot be blank.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "This is a major version upgrade.",
"Trash Can File Versioning": "Trash Can File Versioning",
"Unknown": "Unknown",

View File

@ -196,6 +196,7 @@
<span class="hidden-xs" translate>Syncing</span>
({{syncPercentage(folder.id)}}%)
</span>
<span ng-switch-when="outofsync"><span class="hidden-xs" translate>Out of Sync</span><span class="visible-xs">&#9724;</span></span>
</span>
</h3>
</div>
@ -225,6 +226,17 @@
<a ng-click="showNeed(folder.id)" href="">{{model[folder.id].needFiles | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].needBytes | binary}}B</a>
</td>
</tr>
<tr ng-if="folderStatus(folder) === 'outofsync' || hasFailedFiles(folder.id)">
<th><span class="glyphicon glyphicon-exclamation-sign"></span>&nbsp;<span translate>Failed Items</span></th>
<!-- Show the number of failed items as a link to bring up the list. -->
<td ng-if="hasFailedFiles(folder.id)" class="text-right">
<a ng-click="showFailed(folder.id)" href="">{{failed[folder.id].length | alwaysNumber}}&nbsp;<span translate>items</span></a>
</td>
<!-- The list of failed items hasn't loaded yet; show an ellipsis for the time being. -->
<td ng-if="!hasFailedFiles(folder.id)" class="text-right">
...
</td>
</tr>
<tr ng-if="folder.readOnly">
<th><span class="glyphicon glyphicon-lock"></span>&nbsp;<span translate>Folder Master</span></th>
<td class="text-right">
@ -985,7 +997,7 @@
<table class="table table-striped table-condensed">
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="neededTotal">
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="neededTotal" pagination-id="needed">
<!-- Icon -->
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[f.action]}}"></span> {{needActions[f.action]}}</td>
@ -1018,15 +1030,37 @@
</tr>
</table>
<dir-pagination-controls on-page-change="neededPageChanged(newPageNumber)"></dir-pagination-controls>
<dir-pagination-controls on-page-change="neededPageChanged(newPageNumber)" pagination-id="needed"></dir-pagination-controls>
<ul class="pagination pull-right">
<li ng-repeat="option in [10, 20, 30, 50, 100]" ng-class="{ active: neededPageSize == option }">
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: neededPageSize == option }">
<a href="#" ng-click="neededChangePageSize(option)">{{option}}</a>
<li>
</ul>
<div class="clearfix"></div>
</modal>
<!-- Failed Items modal -->
<modal id="failed" large="yes" status="warning" icon="exclamation-sign" close="yes" title="{{'Failed Items' | translate}}">
<p>
<span translate>The following items could not be synchronized.</span>
<span translate>They are retried automatically and will be synced when the error is resolved.</span>
</p>
<table class="table table-striped table-condensed">
<tr dir-paginate="e in failedCurrent | itemsPerPage: failedPageSize" current-page="failedCurrentPage" pagination-id="failed">
<td><abbr title="{{e.path}}">{{e.path | basename}}</abbr></td>
<td><abbr title="{{e.error}}">{{e.error | lastErrorComponent}}</abbr></td>
</tr>
</table>
<dir-pagination-controls on-page-change="failedPageChanged(newPageNumber)" pagination-id="failed"></dir-pagination-controls>
<ul class="pagination pull-right">
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: failedPageSize == option }">
<a href="#" ng-click="failedChangePageSize(option)">{{option}}</a>
<li>
</ul>
<div class="clearfix"></div>
</modal>
<!-- About modal -->
<modal id="about" large="yes" close="yes" status="info" title="{{'About' | translate}}">
@ -1138,6 +1172,7 @@
<script src="scripts/syncthing/core/filters/binaryFilter.js"></script>
<script src="scripts/syncthing/core/filters/durationFilter.js"></script>
<script src="scripts/syncthing/core/filters/naturalFilter.js"></script>
<script src="scripts/syncthing/core/filters/lastErrorComponentFilter.js"></script>
<script src="scripts/syncthing/core/services/localeService.js"></script>
<script src="assets/lang/valid-langs.js"></script>

View File

@ -18,8 +18,7 @@ angular.module('syncthing.core')
Events.start();
}
// pubic/scope definitions
// public/scope definitions
$scope.completion = {};
$scope.config = {};
@ -47,6 +46,10 @@ angular.module('syncthing.core')
$scope.neededPageSize = 10;
$scope.foldersTotalLocalBytes = 0;
$scope.foldersTotalLocalFiles = 0;
$scope.failed = {};
$scope.failedCurrentPage = 1;
$scope.failedCurrentFolder = undefined;
$scope.failedPageSize = 10;
$(window).bind('beforeunload', function () {
navigatingAway = true;
@ -144,6 +147,13 @@ angular.module('syncthing.core')
if ($scope.model[data.folder]) {
$scope.model[data.folder].state = data.to;
$scope.model[data.folder].error = data.error;
// If a folder has started syncing, then any old list of
// errors is obsolete. We may get a new list of errors very
// shortly though.
if (data.to === 'syncing') {
$scope.failed[data.folder] = [];
}
}
});
@ -151,14 +161,6 @@ angular.module('syncthing.core')
refreshFolderStats();
});
/* currently not using
$scope.$on('Events.REMOTE_INDEX_UPDATED', function (event, arg) {
// Nothing
});
*/
$scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
delete $scope.connections[arg.data.id];
refreshDeviceStats();
@ -284,6 +286,11 @@ angular.module('syncthing.core')
$scope.completion[data.device]._total = tot / cnt;
});
$scope.$on(Events.FOLDER_ERRORS, function (event, arg) {
var data = arg.data;
$scope.failed[data.folder] = data.errors;
});
$scope.emitHTTPError = function (data, status, headers, config) {
$scope.$emit('HTTPError', {data: data, status: status, headers: headers, config: config});
};
@ -492,6 +499,14 @@ angular.module('syncthing.core')
refreshNeed($scope.neededFolder);
};
$scope.failedPageChanged = function (page) {
$scope.failedCurrentPage = page;
};
$scope.failedChangePageSize = function (perpage) {
$scope.failedPageSize = perpage;
};
var refreshDeviceStats = debounce(function () {
$http.get(urlbase + "/stats/device").success(function (data) {
$scope.deviceStats = data;
@ -526,6 +541,11 @@ angular.module('syncthing.core')
return 'unknown';
}
// after restart syncthing process state may be empty
if (!$scope.model[folderCfg.id].state) {
return 'unknown';
}
if (folderCfg.devices.length <= 1) {
return 'unshared';
}
@ -534,47 +554,36 @@ angular.module('syncthing.core')
return 'stopped';
}
if ($scope.model[folderCfg.id].state == 'error') {
var state = '' + $scope.model[folderCfg.id].state;
if (state === 'error') {
return 'stopped'; // legacy, the state is called "stopped" in the GUI
}
// after restart syncthing process state may be empty
if (!$scope.model[folderCfg.id].state) {
return 'unknown';
if (state === 'idle' && $scope.model[folderCfg.id].needFiles > 0) {
return 'outofsync';
}
return '' + $scope.model[folderCfg.id].state;
return state;
};
$scope.folderClass = function (folderCfg) {
if (typeof $scope.model[folderCfg.id] === 'undefined') {
// Unknown
return 'info';
}
var status = $scope.folderStatus(folderCfg);
if (folderCfg.devices.length <= 1) {
// Unshared
return 'warning';
}
if ($scope.model[folderCfg.id].invalid !== '') {
// Errored
return 'danger';
}
var state = '' + $scope.model[folderCfg.id].state;
if (state == 'idle') {
if (status == 'idle') {
return 'success';
}
if (state == 'syncing') {
if (status == 'syncing' || status == 'scanning') {
return 'primary';
}
if (state == 'scanning') {
return 'primary';
if (status === 'unknown') {
return 'info';
}
if (state == 'error') {
if (status === 'unshared') {
return 'warning';
}
if (status === 'stopped' || status === 'outofsync' || status === 'error') {
return 'danger';
}
return 'info';
};
@ -1277,6 +1286,23 @@ angular.module('syncthing.core')
});
};
$scope.showFailed = function (folder) {
$scope.failedCurrent = $scope.failed[folder]
$('#failed').modal().on('hidden.bs.modal', function () {
$scope.failedCurrent = undefined;
});
};
$scope.hasFailedFiles = function (folder) {
if (!$scope.failed[folder]) {
return false;
}
if ($scope.failed[folder].length == 0) {
return false;
}
return true
};
$scope.override = function (folder) {
$http.post(urlbase + "/db/override?folder=" + encodeURIComponent(folder));
};

View File

@ -0,0 +1,12 @@
angular.module('syncthing.core')
.filter('lastErrorComponent', function () {
return function (input) {
if (input === undefined)
return "";
var parts = input.split(/:\s*/);
if (!parts || parts.length < 1) {
return input;
}
return parts[parts.length - 1];
};
});

View File

@ -75,6 +75,7 @@ angular.module('syncthing.core')
STARTING: 'Starting', // Emitted exactly once, when Syncthing starts, before parsing configuration etc
STARTUP_COMPLETED: 'StartupCompleted', // Emitted exactly once, when initialization is complete and Syncthing is ready to start exchanging data with other devices
STATE_CHANGED: 'StateChanged', // Emitted when a folder changes state
FOLDER_ERRORS: 'FolderErrors', // Emitted when a folder has errors preventing a full sync
start: function() {
$http.get(urlbase + '/events?limit=1')

File diff suppressed because one or more lines are too long