Kaynağa Gözat

Show FolderErrors result in UI (fixes #1437)

Jakob Borg 10 yıl önce
ebeveyn
işleme
60004ebff1

+ 4 - 1
gui/assets/lang/lang-en.json

@@ -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",

+ 38 - 3
gui/index.html

@@ -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>

+ 62 - 36
gui/scripts/syncthing/core/controllers/syncthingController.js

@@ -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';
-            }
-
-            if (folderCfg.devices.length <= 1) {
-                // Unshared
-                return 'warning';
-            }
-
-            if ($scope.model[folderCfg.id].invalid !== '') {
-                // Errored
-                return 'danger';
-            }
+            var status = $scope.folderStatus(folderCfg);
 
-            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 (status === 'unshared') {
+                return 'warning';
             }
-            if (state == 'error') {
+            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));
         };

+ 12 - 0
gui/scripts/syncthing/core/filters/lastErrorComponentFilter.js

@@ -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];
+        };
+    });

+ 1 - 0
gui/scripts/syncthing/core/services/events.js

@@ -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')

Dosya farkı çok büyük olduğundan ihmal edildi
+ 2 - 2
internal/auto/gui.files.go


Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor