summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAudrius Butkevicius <audrius.butkevicius@gmail.com>2018-01-01 14:39:23 +0000
committerJakob Borg <jakob@kastelo.net>2018-01-01 15:39:23 +0100
commitb0e2050cdbc539dcffaeb9e7cb88869ce008a8c2 (patch)
tree8789b42582fdcec6bc35dd0a2cdafeca9b69795d
parentc7f136c2b8400fc2e972a9be52d44db9452cf717 (diff)
cmd/syncthing: UI for version restoration (fixes #2599) (#4602)
cmd/syncthing: Add UI for version restoration (fixes #2599)
-rw-r--r--cmd/syncthing/gui.go40
-rw-r--r--cmd/syncthing/mocked_model_test.go9
-rw-r--r--gui/black/assets/css/theme.css4
-rw-r--r--gui/dark/assets/css/theme.css3
-rw-r--r--gui/default/assets/css/overrides.css4
-rw-r--r--gui/default/assets/css/theme.css6
-rw-r--r--gui/default/assets/lang/lang-en.json11
-rw-r--r--gui/default/index.html10
-rw-r--r--gui/default/syncthing/app.js72
-rwxr-xr-xgui/default/syncthing/core/syncthingController.js237
-rw-r--r--gui/default/syncthing/device/removeDeviceDialogView.html26
-rw-r--r--gui/default/syncthing/folder/removeFolderDialogView.html32
-rw-r--r--gui/default/syncthing/folder/restoreVersionsConfirmation.html15
-rw-r--r--gui/default/syncthing/folder/restoreVersionsMassActions.html11
-rw-r--r--gui/default/syncthing/folder/restoreVersionsModalView.html51
-rw-r--r--gui/default/syncthing/folder/restoreVersionsVersionSelector.html17
-rw-r--r--gui/default/vendor/bootstrap/css/daterangepicker.css269
-rw-r--r--gui/default/vendor/bootstrap/js/daterangepicker.js1626
-rw-r--r--gui/default/vendor/fancytree/css/ui.fancytree.css663
-rw-r--r--gui/default/vendor/fancytree/jquery.fancytree-all-deps.js12045
-rw-r--r--gui/default/vendor/fancytree/skin-lion/icons.gifbin0 -> 5937 bytes
-rw-r--r--gui/default/vendor/fancytree/skin-lion/loading.gifbin0 -> 1849 bytes
-rw-r--r--gui/default/vendor/fancytree/skin-lion/vline.gifbin0 -> 852 bytes
-rw-r--r--gui/default/vendor/moment/moment.js4517
-rw-r--r--lib/config/folderconfiguration.go13
-rw-r--r--lib/model/model.go158
-rw-r--r--lib/model/model_test.go213
-rw-r--r--lib/model/rwfolder.go2
-rw-r--r--lib/versioner/simple.go4
-rw-r--r--lib/versioner/simple_test.go4
-rw-r--r--lib/versioner/staggered.go17
-rw-r--r--lib/versioner/util.go19
-rw-r--r--lib/versioner/versioner.go12
33 files changed, 20045 insertions, 65 deletions
diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go
index ad39a8c2a..2784aa11b 100644
--- a/cmd/syncthing/gui.go
+++ b/cmd/syncthing/gui.go
@@ -40,6 +40,7 @@ import (
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade"
+ "github.com/syncthing/syncthing/lib/versioner"
"github.com/vitrun/qart/qr"
"golang.org/x/crypto/bcrypt"
)
@@ -95,6 +96,8 @@ type modelIntf interface {
ResetFolder(folder string)
Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []model.Availability
GetIgnores(folder string) ([]string, []string, error)
+ GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error)
+ RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error)
SetIgnores(folder string, content []string) error
DelayScan(folder string, next time.Duration)
ScanFolder(folder string) error
@@ -259,6 +262,7 @@ func (s *apiService) Serve() {
getRestMux.HandleFunc("/rest/db/remoteneed", s.getDBRemoteNeed) // device folder [perpage] [page]
getRestMux.HandleFunc("/rest/db/status", s.getDBStatus) // folder
getRestMux.HandleFunc("/rest/db/browse", s.getDBBrowse) // folder [prefix] [dirsonly] [levels]
+ getRestMux.HandleFunc("/rest/folder/versions", s.getFolderVersions) // folder
getRestMux.HandleFunc("/rest/events", s.getIndexEvents) // [since] [limit] [timeout] [events]
getRestMux.HandleFunc("/rest/events/disk", s.getDiskEvents) // [since] [limit] [timeout]
getRestMux.HandleFunc("/rest/stats/device", s.getDeviceStats) // -
@@ -287,6 +291,7 @@ func (s *apiService) Serve() {
postRestMux.HandleFunc("/rest/db/ignores", s.postDBIgnores) // folder
postRestMux.HandleFunc("/rest/db/override", s.postDBOverride) // folder
postRestMux.HandleFunc("/rest/db/scan", s.postDBScan) // folder [sub...] [delay]
+ postRestMux.HandleFunc("/rest/folder/versions", s.postFolderVersionsRestore) // folder <body>
postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig) // <body>
postRestMux.HandleFunc("/rest/system/error", s.postSystemError) // <body>
postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear) // -
@@ -1309,6 +1314,41 @@ func (s *apiService) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
sendJSON(w, comp)
}
+func (s *apiService) getFolderVersions(w http.ResponseWriter, r *http.Request) {
+ qs := r.URL.Query()
+ versions, err := s.model.GetFolderVersions(qs.Get("folder"))
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ sendJSON(w, versions)
+}
+
+func (s *apiService) postFolderVersionsRestore(w http.ResponseWriter, r *http.Request) {
+ qs := r.URL.Query()
+
+ bs, err := ioutil.ReadAll(r.Body)
+ r.Body.Close()
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+
+ var versions map[string]time.Time
+ err = json.Unmarshal(bs, &versions)
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+
+ ferr, err := s.model.RestoreFolderVersions(qs.Get("folder"), versions)
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ sendJSON(w, ferr)
+}
+
func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
current := qs.Get("current")
diff --git a/cmd/syncthing/mocked_model_test.go b/cmd/syncthing/mocked_model_test.go
index f6e36d030..e6c6949d5 100644
--- a/cmd/syncthing/mocked_model_test.go
+++ b/cmd/syncthing/mocked_model_test.go
@@ -14,6 +14,7 @@ import (
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/stats"
+ "github.com/syncthing/syncthing/lib/versioner"
)
type mockedModel struct{}
@@ -75,6 +76,14 @@ func (m *mockedModel) SetIgnores(folder string, content []string) error {
return nil
}
+func (m *mockedModel) GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error) {
+ return nil, nil
+}
+
+func (m *mockedModel) RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error) {
+ return nil, nil
+}
+
func (m *mockedModel) PauseDevice(device protocol.DeviceID) {
}
diff --git a/gui/black/assets/css/theme.css b/gui/black/assets/css/theme.css
index 4a84eb561..3092c6cad 100644
--- a/gui/black/assets/css/theme.css
+++ b/gui/black/assets/css/theme.css
@@ -243,3 +243,7 @@ code.ng-binding{
.progress .frontal {
color: #222;
}
+
+.fancytree-title {
+ color: #aaa !important;
+}
diff --git a/gui/dark/assets/css/theme.css b/gui/dark/assets/css/theme.css
index 62fb99001..3086ac179 100644
--- a/gui/dark/assets/css/theme.css
+++ b/gui/dark/assets/css/theme.css
@@ -256,3 +256,6 @@ code.ng-binding{
color: #3fa9f0;
}
+.fancytree-title {
+ color: #aaa !important;
+}
diff --git a/gui/default/assets/css/overrides.css b/gui/default/assets/css/overrides.css
index b35f5f571..32bef71f3 100644
--- a/gui/default/assets/css/overrides.css
+++ b/gui/default/assets/css/overrides.css
@@ -371,3 +371,7 @@ ul.three-columns li, ul.two-columns li {
.tab-content {
padding-top: 10px;
}
+
+.fancytree-ext-table {
+ width: 100% !important;
+}
diff --git a/gui/default/assets/css/theme.css b/gui/default/assets/css/theme.css
index 56dbc074d..e54b06517 100644
--- a/gui/default/assets/css/theme.css
+++ b/gui/default/assets/css/theme.css
@@ -27,3 +27,9 @@
.panel-heading:hover, .panel-heading:focus {
text-decoration: none;
}
+
+.fancytree-ext-filter-hide tr.fancytree-submatch span.fancytree-title,
+.fancytree-ext-filter-hide span.fancytree-node.fancytree-submatch span.fancytree-title {
+ color: black !important;
+ font-weight: lighter !important;
+}
diff --git a/gui/default/assets/lang/lang-en.json b/gui/default/assets/lang/lang-en.json
index 067929d52..29d301007 100644
--- a/gui/default/assets/lang/lang-en.json
+++ b/gui/default/assets/lang/lang-en.json
@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Any devices configured on an introducer device will be added to this device as well.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
+ "Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatic upgrade now offers the choice between stable releases and release candidates.",
"Automatic upgrades": "Automatic upgrades",
@@ -67,6 +68,8 @@
"Discovered": "Discovered",
"Discovery": "Discovery",
"Discovery Failures": "Discovery Failures",
+ "Do not restore": "Do not restore",
+ "Do not restore all": "Do not restore all",
"Documentation": "Documentation",
"Download Rate": "Download Rate",
"Downloaded": "Downloaded",
@@ -95,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.",
"Filesystem Notifications": "Filesystem Notifications",
+ "Filter by date": "Filter by date",
+ "Filter by name": "Filter by name",
"Folder": "Folder",
"Folder ID": "Folder ID",
"Folder Label": "Folder Label",
@@ -141,6 +146,7 @@
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Major Upgrade",
+ "Mass actions": "Mass actions",
"Master": "Master",
"Maximum Age": "Maximum Age",
"Metadata Only": "Metadata Only",
@@ -201,6 +207,8 @@
"Restart": "Restart",
"Restart Needed": "Restart Needed",
"Restarting": "Restarting",
+ "Restore": "Restore",
+ "Restore Versions": "Restore Versions",
"Resume": "Resume",
"Resume All": "Resume All",
"Reused": "Reused",
@@ -210,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
+ "Select latest version": "Select latest version",
+ "Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Select the devices to share this folder with.",
"Select the folders to share with this device.": "Select the folders to share with this device.",
"Send \u0026 Receive": "Send \u0026 Receive",
@@ -232,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Size": "Size",
"Smallest First": "Smallest First",
+ "Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Source Code",
"Stable releases and release candidates": "Stable releases and release candidates",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.",
diff --git a/gui/default/index.html b/gui/default/index.html
index 193f126f8..1a74a4547 100644
--- a/gui/default/index.html
+++ b/gui/default/index.html
@@ -19,10 +19,12 @@
<title ng-bind="thisDeviceName() + ' | Syncthing'"></title>
<link href="vendor/bootstrap/css/bootstrap.css" rel="stylesheet"/>
+ <link href="vendor/bootstrap/css/daterangepicker.css" rel="stylesheet"/>
<link href="assets/font/raleway.css" rel="stylesheet"/>
<link href="vendor/font-awesome/css/font-awesome.css" rel="stylesheet"/>
<link href="assets/css/overrides.css" rel="stylesheet"/>
<link href="assets/css/theme.css" rel="stylesheet"/>
+ <link href="vendor/fancytree/css/ui.fancytree.css" rel="stylesheet"/>
</head>
<body>
@@ -434,6 +436,9 @@
<button ng-if="folder.paused" type="button" class="btn btn-sm btn-default" ng-click="setFolderPause(folder.id, false)">
<span class="fa fa-play"></span>&nbsp;<span translate>Resume</span>
</button>
+ <button type="button" class="btn btn-default btn-sm" ng-click="restoreVersions.show(folder.id)" ng-if="folder.versioning.type">
+ <span class="fa fa-undo"></span>&nbsp;<span translate>Versions</span>
+ </button>
<button type="button" class="btn btn-sm btn-default" ng-click="rescanFolder(folder.id)" ng-show="['idle', 'stopped', 'unshared'].indexOf(folderStatus(folder)) > -1">
<span class="fa fa-refresh"></span>&nbsp;<span translate>Rescan</span>
</button>
@@ -723,6 +728,8 @@
<ng-include src="'syncthing/device/globalChangesModalView.html'"></ng-include>
<ng-include src="'syncthing/folder/editFolderModalView.html'"></ng-include>
<ng-include src="'syncthing/folder/editIgnoresModalView.html'"></ng-include>
+ <ng-include src="'syncthing/folder/restoreVersionsModalView.html'"></ng-include>
+ <ng-include src="'syncthing/folder/restoreVersionsConfirmation.html'"></ng-include>
<ng-include src="'syncthing/settings/settingsModalView.html'"></ng-include>
<ng-include src="'syncthing/settings/advancedSettingsModalView.html'"></ng-include>
<ng-include src="'syncthing/usagereport/usageReportModalView.html'"></ng-include>
@@ -744,7 +751,10 @@
<script type="text/javascript" src="vendor/angular/angular-translate.js"></script>
<script type="text/javascript" src="vendor/angular/angular-translate-loader-static-files.js"></script>
<script type="text/javascript" src="vendor/angular/angular-dirPagination.js"></script>
+ <script type="text/javascript" src="vendor/moment/moment.js"></script>
<script type="text/javascript" src="vendor/bootstrap/js/bootstrap.js"></script>
+ <script type="text/javascript" src="vendor/bootstrap/js/daterangepicker.js"></script>
+ <script type="text/javascript" src="vendor/fancytree/jquery.fancytree-all-deps.js"></script>
<!-- / vendor scripts -->
<!-- gui application code -->
diff --git a/gui/default/syncthing/app.js b/gui/default/syncthing/app.js
index 6cd12b443..ac9c89cb8 100644
--- a/gui/default/syncthing/app.js
+++ b/gui/default/syncthing/app.js
@@ -134,3 +134,75 @@ function debounce(func, wait) {
return result;
};
}
+
+function buildTree(children) {
+ /* Converts
+ *
+ * {
+ * 'foo/bar': [...],
+ * 'foo/baz': [...]
+ * }
+ *
+ * to
+ *
+ * [
+ * {
+ * title: 'foo',
+ * children: [
+ * {
+ * title: 'bar',
+ * versions: [...],
+ * ...
+ * },
+ * {
+ * title: 'baz',
+ * versions: [...],
+ * ...
+ * }
+ * ],
+ * }
+ * ]
+ */
+ var root = {
+ children: []
+ }
+
+ $.each(children, function(path, data) {
+ var parts = path.split('/');
+ var name = parts.splice(-1)[0];
+
+ var keySoFar = [];
+ var parent = root;
+ while (parts.length > 0) {
+ var part = parts.shift();
+ keySoFar.push(part);
+ var found = false;
+ for (var i = 0; i < parent.children.length; i++) {
+ if (parent.children[i].title == part) {
+ parent = parent.children[i];
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ var child = {
+ title: part,
+ key: keySoFar.join('/'),
+ folder: true,
+ children: []
+ }
+ parent.children.push(child);
+ parent = child;
+ }
+ }
+
+ parent.children.push({
+ title: name,
+ key: path,
+ folder: false,
+ versions: data,
+ });
+ });
+
+ return root.children;
+}
diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js
index 78c7cadab..8e27440ff 100755
--- a/gui/default/syncthing/core/syncthingController.js
+++ b/gui/default/syncthing/core/syncthingController.js
@@ -2,7 +2,7 @@ angular.module('syncthing.core')
.config(function($locationProvider) {
$locationProvider.html5Mode({enabled: true, requireBase: false}).hashPrefix('!');
})
- .controller('SyncthingController', function ($scope, $http, $location, LocaleService, Events, $filter, $q, $interval) {
+ .controller('SyncthingController', function ($scope, $http, $location, LocaleService, Events, $filter, $q, $compile, $timeout, $rootScope) {
'use strict';
// private/helper definitions
@@ -1107,9 +1107,9 @@ angular.module('syncthing.core')
},
show: function() {
$scope.logging.refreshFacilities();
- $scope.logging.timer = $interval($scope.logging.fetch, 0, 1);
+ $scope.logging.timer = $timeout($scope.logging.fetch);
$('#logViewer').modal().on('hidden.bs.modal', function () {
- $interval.cancel($scope.logging.timer);
+ $timeout.cancel($scope.logging.timer);
$scope.logging.timer = null;
$scope.logging.entries = [];
});
@@ -1138,7 +1138,7 @@ angular.module('syncthing.core')
var textArea = $('#logViewerText');
if (textArea.is(":focus")) {
if (!$scope.logging.timer) return;
- $scope.logging.timer = $interval($scope.logging.fetch, 500, 1);
+ $scope.logging.timer = $timeout($scope.logging.fetch, 500);
return;
}
@@ -1149,7 +1149,7 @@ angular.module('syncthing.core')
$http.get(urlbase + '/system/log' + (last ? '?since=' + encodeURIComponent(last) : '')).success(function (data) {
if (!$scope.logging.timer) return;
- $scope.logging.timer = $interval($scope.logging.fetch, 2000, 1);
+ $scope.logging.timer = $timeout($scope.logging.fetch, 2000);
if (!textArea.is(":focus")) {
if (data.messages) {
$scope.logging.entries.push.apply($scope.logging.entries, data.messages);
@@ -1767,6 +1767,233 @@ angular.module('syncthing.core')
});
};
+ function resetRestoreVersions() {
+ $scope.restoreVersions = {
+ folder: null,
+ selections: {},
+ versions: null,
+ tree: null,
+ errors: null,
+ filters: {},
+ massAction: function (name, action) {
+ $.each($scope.restoreVersions.versions, function(key) {
+ if (key.startsWith(name + '/') && (!$scope.restoreVersions.filters.text || key.indexOf($scope.restoreVersions.filters.text) > -1)) {
+ if (action == 'unset') {
+ delete $scope.restoreVersions.selections[key];
+ return;
+ }
+
+ var availableVersions = [];
+ $.each($scope.restoreVersions.filterVersions($scope.restoreVersions.versions[key]), function(idx, version) {
+ availableVersions.push(version.versionTime);
+ })
+
+ if (availableVersions.length) {
+ availableVersions.sort(function (a, b) { return a - b; });
+ if (action == 'latest') {
+ $scope.restoreVersions.selections[key] = availableVersions.pop();
+ } else if (action == 'oldest') {
+ $scope.restoreVersions.selections[key] = availableVersions.shift();
+ }
+ }
+ }
+ });
+ },
+ filterVersions: function(versions) {
+ var filteredVersions = [];
+ $.each(versions, function (idx, version) {
+ if (moment(version.versionTime).isBetween($scope.restoreVersions.filters['start'], $scope.restoreVersions.filters['end'], null, '[]')) {
+ filteredVersions.push(version);
+ }
+ });
+ return filteredVersions;
+ },
+ selectionCount: function() {
+ var count = 0;
+ $.each($scope.restoreVersions.selections, function(key, value) {
+ if (value) {
+ count++;
+ }
+ });
+ return count;
+ },
+
+ restore: function() {
+ $scope.restoreVersions.tree.clear();
+ $scope.restoreVersions.tree = null;
+ $scope.restoreVersions.versions = null;
+ var selections = {};
+ $.each($scope.restoreVersions.selections, function(key, value) {
+ if (value) {
+ selections[key] = value;
+ }
+ });
+ $scope.restoreVersions.selections = {};
+
+ $http.post(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder), selections).success(function (data) {
+ if (Object.keys(data).length == 0) {
+ $('#restoreVersions').modal('hide');
+ } else {
+ $scope.restoreVersions.errors = data;
+ }
+ });
+ },
+ show: function(folder) {
+ $scope.restoreVersions.folder = folder;
+
+ var closed = false;
+ var modalShown = $q.defer();
+ $('#restoreVersions').modal().on('hidden.bs.modal', function () {
+ closed = true;
+ resetRestoreVersions();
+ }).on('shown.bs.modal', function() {
+ modalShown.resolve();
+ });
+
+ var dataReceived = $http.get(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder))
+ .success(function (data) {
+ $.each(data, function(key, values) {
+ $.each(values, function(idx, value) {
+ value.modTime = new Date(value.modTime);
+ value.versionTime = new Date(value.versionTime);
+ });
+ });
+ if (closed) return;
+ $scope.restoreVersions.versions = data;
+ });
+
+ $q.all([dataReceived, modalShown.promise]).then(function() {
+ if (closed) {
+ resetRestoreVersions();
+ return;
+ }
+
+ $scope.restoreVersions.tree = $("#restoreTree").fancytree({
+ extensions: ["table", "filter"],
+ quicksearch: true,
+ filter: {
+ autoApply: true,
+ counter: true,
+ hideExpandedCounter: true,
+ hideExpanders: true,
+ highlight: true,
+ leavesOnly: false,
+ nodata: true,
+ mode: "hide"
+ },
+ table: {
+ indentation: 20,
+ nodeColumnIdx: 0,
+ },
+ debugLevel: 2,
+ source: buildTree($scope.restoreVersions.versions),
+ renderColumns: function(event, data) {
+ var node = data.node,
+ $tdList = $(node.tr).find(">td"),
+ template;
+ if (node.folder) {
+ template = '<div ng-include="\'syncthing/folder/restoreVersionsMassActions.html\'" class="pull-right"/>';
+ } else {
+ template = '<div ng-include="\'syncthing/folder/restoreVersionsVersionSelector.html\'" class="pull-right"/>';
+ }
+
+ var scope = $rootScope.$new(true);
+ scope.key = node.key;
+ scope.restoreVersions = $scope.restoreVersions;
+
+ $tdList.eq(1).html(
+ $compile(template)(scope)
+ );
+
+ // Force angular to redraw.
+ $timeout(function() {
+ $scope.$apply();
+ });
+ }
+ }).fancytree("getTree");
+
+ var minDate = moment(),
+ maxDate = moment(0, 'X'),
+ date;
+
+ // Find version window.
+ $.each($scope.restoreVersions.versions, function(key) {
+ $.each($scope.restoreVersions.versions[key], function(idx, version) {
+ date = moment(version.versionTime);
+ if (date.isBefore(minDate)) {
+ minDate = date;
+ }
+ if (date.isAfter(maxDate)) {
+ maxDate = date;
+ }
+ });
+ });
+
+ $scope.restoreVersions.filters['start'] = minDate;
+ $scope.restoreVersions.filters['end'] = maxDate;
+
+ var ranges = {
+ 'All time': [minDate, maxDate],
+ 'Today': [moment(), moment()],
+ 'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
+ 'Last 7 Days': [moment().subtract(6, 'days'), moment()],
+ 'Last 30 Days': [moment().subtract(29, 'days'), moment()],
+ 'This Month': [moment().startOf('month'), moment().endOf('month')],
+ 'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
+ };
+
+ // Filter out invalid ranges.
+ $.each(ranges, function(key, range) {
+ if (!range[0].isBetween(minDate, maxDate, null, '[]') && !range[1].isBetween(minDate, maxDate, null, '[]')) {
+ delete ranges[key];
+ }
+ });
+
+ $("#restoreVersionDateRange").daterangepicker({
+ timePicker: true,
+ timePicker24Hour: true,
+ timePickerSeconds: true,
+