diff --git a/app/__module.js b/app/__module.js
index e1cf659fa..00be9300d 100644
--- a/app/__module.js
+++ b/app/__module.js
@@ -15,6 +15,7 @@ angular.module('portainer', [
'angular-json-tree',
'angular-loading-bar',
'angular-clipboard',
+ 'ngFileSaver',
'luegg.directives',
'portainer.templates',
'portainer.app',
diff --git a/app/agent/components/volume-browser/volume-browser-datatable/volume-browser-datatable.js b/app/agent/components/volume-browser/volume-browser-datatable/volume-browser-datatable.js
new file mode 100644
index 000000000..e3139974d
--- /dev/null
+++ b/app/agent/components/volume-browser/volume-browser-datatable/volume-browser-datatable.js
@@ -0,0 +1,15 @@
+angular.module('portainer.agent').component('volumeBrowserDatatable', {
+ templateUrl: 'app/agent/components/volume-browser/volume-browser-datatable/volumeBrowserDatatable.html',
+ controller: 'GenericDatatableController',
+ bindings: {
+ titleText: '@',
+ titleIcon: '@',
+ dataset: '<',
+ tableKey: '@',
+ orderBy: '@',
+ reverseOrder: '<'
+ },
+ require: {
+ volumeBrowser: '^^volumeBrowser'
+ }
+});
diff --git a/app/agent/components/volume-browser/volume-browser-datatable/volumeBrowserDatatable.html b/app/agent/components/volume-browser/volume-browser-datatable/volumeBrowserDatatable.html
new file mode 100644
index 000000000..7599d7caf
--- /dev/null
+++ b/app/agent/components/volume-browser/volume-browser-datatable/volumeBrowserDatatable.html
@@ -0,0 +1,90 @@
+
diff --git a/app/agent/components/volume-browser/volume-browser.js b/app/agent/components/volume-browser/volume-browser.js
new file mode 100644
index 000000000..964803da0
--- /dev/null
+++ b/app/agent/components/volume-browser/volume-browser.js
@@ -0,0 +1,8 @@
+angular.module('portainer.agent').component('volumeBrowser', {
+ templateUrl: 'app/agent/components/volume-browser/volumeBrowser.html',
+ controller: 'VolumeBrowserController',
+ bindings: {
+ volumeId: '<',
+ nodeName: '<'
+ }
+});
diff --git a/app/agent/components/volume-browser/volumeBrowser.html b/app/agent/components/volume-browser/volumeBrowser.html
new file mode 100644
index 000000000..643d8c88b
--- /dev/null
+++ b/app/agent/components/volume-browser/volumeBrowser.html
@@ -0,0 +1,5 @@
+
diff --git a/app/agent/components/volume-browser/volumeBrowserController.js b/app/agent/components/volume-browser/volumeBrowserController.js
new file mode 100644
index 000000000..2fa4426b9
--- /dev/null
+++ b/app/agent/components/volume-browser/volumeBrowserController.js
@@ -0,0 +1,115 @@
+angular.module('portainer.agent')
+.controller('VolumeBrowserController', ['HttpRequestHelper', 'VolumeBrowserService', 'FileSaver', 'Blob', 'ModalService', 'Notifications',
+function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications) {
+ var ctrl = this;
+
+ this.state = {
+ path: '/'
+ };
+
+ this.rename = function(file, newName) {
+ var filePath = this.state.path === '/' ? file : this.state.path + '/' + file;
+ var newFilePath = this.state.path === '/' ? newName : this.state.path + '/' + newName;
+
+ VolumeBrowserService.rename(this.volumeId, filePath, newFilePath)
+ .then(function success() {
+ Notifications.success('File successfully renamed', newFilePath);
+ return VolumeBrowserService.ls(ctrl.volumeId, ctrl.state.path);
+ })
+ .then(function success(data) {
+ ctrl.files = data;
+ })
+ .catch(function error(err) {
+ Notifications.error('Failure', err, 'Unable to rename file');
+ });
+ };
+
+ this.delete = function(file) {
+ var filePath = this.state.path === '/' ? file : this.state.path + '/' + file;
+
+ ModalService.confirmDeletion(
+ 'Are you sure that you want to delete ' + filePath + ' ?',
+ function onConfirm(confirmed) {
+ if(!confirmed) { return; }
+ deleteFile(filePath);
+ }
+ );
+ };
+
+ this.download = function(file) {
+ var filePath = this.state.path === '/' ? file : this.state.path + '/' + file;
+ VolumeBrowserService.get(this.volumeId, filePath)
+ .then(function success(data) {
+ var downloadData = new Blob([data.file], { type: 'text/plain;charset=utf-8' });
+ FileSaver.saveAs(downloadData, file);
+ })
+ .catch(function error(err) {
+ Notifications.error('Failure', err, 'Unable to download file');
+ });
+ };
+
+ this.up = function() {
+ var parentFolder = parentPath(this.state.path);
+ browse(parentFolder);
+ };
+
+ this.browse = function(folder) {
+ var path = buildPath(this.state.path, folder);
+ browse(path);
+ };
+
+ function deleteFile(file) {
+ VolumeBrowserService.delete(ctrl.volumeId, file)
+ .then(function success() {
+ Notifications.success('File successfully deleted', file);
+ return VolumeBrowserService.ls(ctrl.volumeId, ctrl.state.path);
+ })
+ .then(function success(data) {
+ ctrl.files = data;
+ })
+ .catch(function error(err) {
+ Notifications.error('Failure', err, 'Unable to delete file');
+ });
+ }
+
+
+ function browse(path) {
+ VolumeBrowserService.ls(ctrl.volumeId, path)
+ .then(function success(data) {
+ ctrl.state.path = path;
+ ctrl.files = data;
+ })
+ .catch(function error(err) {
+ Notifications.error('Failure', err, 'Unable to browse volume');
+ });
+ }
+
+ function parentPath(path) {
+ if (path.lastIndexOf('/') === 0) {
+ return '/';
+ }
+
+ var split = _.split(path, '/');
+ return _.join(_.slice(split, 0, split.length - 1), '/');
+ }
+
+ function buildPath(parent, file) {
+ if (parent === '/') {
+ return parent + file;
+ }
+ return parent + '/' + file;
+ }
+
+
+ this.$onInit = function() {
+ HttpRequestHelper.setPortainerAgentTargetHeader(this.nodeName);
+ VolumeBrowserService.ls(this.volumeId, this.state.path)
+ .then(function success(data) {
+ ctrl.files = data;
+ })
+ .catch(function error(err) {
+ Notifications.error('Failure', err, 'Unable to browse volume');
+ });
+ };
+
+}]);
diff --git a/app/agent/rest/agent.js b/app/agent/rest/agent.js
index 04d974780..a7800717c 100644
--- a/app/agent/rest/agent.js
+++ b/app/agent/rest/agent.js
@@ -5,6 +5,6 @@ angular.module('portainer.agent')
endpointId: EndpointProvider.endpointID
},
{
- query: {method: 'GET', isArray: true}
+ query: { method: 'GET', isArray: true }
});
}]);
diff --git a/app/agent/rest/browse.js b/app/agent/rest/browse.js
new file mode 100644
index 000000000..b4e8d53e0
--- /dev/null
+++ b/app/agent/rest/browse.js
@@ -0,0 +1,22 @@
+angular.module('portainer.agent')
+.factory('Browse', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
+ 'use strict';
+ return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/browse/:id/:action', {
+ endpointId: EndpointProvider.endpointID
+ },
+ {
+ ls: {
+ method: 'GET', isArray: true, params: { id: '@id', action: 'ls' }
+ },
+ get: {
+ method: 'GET', params: { id: '@id', action: 'get' },
+ transformResponse: browseGetResponse
+ },
+ delete: {
+ method: 'DELETE', params: { id: '@id', action: 'delete' }
+ },
+ rename: {
+ method: 'PUT', params: { id: '@id', action: 'rename' }
+ }
+ });
+}]);
diff --git a/app/agent/rest/response/browse.js b/app/agent/rest/response/browse.js
new file mode 100644
index 000000000..7047777a6
--- /dev/null
+++ b/app/agent/rest/response/browse.js
@@ -0,0 +1,9 @@
+// The get action of the Browse service returns a file.
+// ngResource will transform it as an array of chars.
+// This functions simply creates a response object and assign
+// the data to a field.
+function browseGetResponse(data) {
+ var response = {};
+ response.file = data;
+ return response;
+}
diff --git a/app/agent/services/volumeBrowserService.js b/app/agent/services/volumeBrowserService.js
new file mode 100644
index 000000000..22b020494
--- /dev/null
+++ b/app/agent/services/volumeBrowserService.js
@@ -0,0 +1,27 @@
+angular.module('portainer.agent')
+.factory('VolumeBrowserService', ['$q', 'Browse', function VolumeBrowserServiceFactory($q, Browse) {
+ 'use strict';
+ var service = {};
+
+ service.ls = function(volumeId, path) {
+ return Browse.ls({ 'id': volumeId, 'path': path }).$promise;
+ };
+
+ service.get = function(volumeId, path) {
+ return Browse.get({ 'id': volumeId, 'path': path }).$promise;
+ };
+
+ service.delete = function(volumeId, path) {
+ return Browse.delete({ 'id': volumeId, 'path': path }).$promise;
+ };
+
+ service.rename = function(volumeId, path, newPath) {
+ var payload = {
+ CurrentFilePath: path,
+ NewFilePath: newPath
+ };
+ return Browse.rename({ 'id': volumeId }, payload).$promise;
+ };
+
+ return service;
+}]);
diff --git a/app/docker/__module.js b/app/docker/__module.js
index 78c34897a..c372684de 100644
--- a/app/docker/__module.js
+++ b/app/docker/__module.js
@@ -383,6 +383,17 @@ angular.module('portainer.docker', ['portainer.app'])
}
};
+ var volumeBrowse = {
+ name: 'docker.volumes.volume.browse',
+ url: '/browse',
+ views: {
+ 'content@': {
+ templateUrl: 'app/docker/views/volumes/browse/browsevolume.html',
+ controller: 'BrowseVolumeController'
+ }
+ }
+ };
+
var volumeCreation = {
name: 'docker.volumes.new',
url: '/new',
@@ -430,5 +441,6 @@ angular.module('portainer.docker', ['portainer.app'])
$stateRegistryProvider.register(taskLogs);
$stateRegistryProvider.register(volumes);
$stateRegistryProvider.register(volume);
+ $stateRegistryProvider.register(volumeBrowse);
$stateRegistryProvider.register(volumeCreation);
}]);
diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html
index 58e60e04b..c93f506f4 100644
--- a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html
+++ b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html
@@ -103,7 +103,10 @@
{{ item.Id | truncate:40 }}
- Unused
+
+ browse
+
+ Unused
{{ item.StackName ? item.StackName : '-' }} |
{{ item.Driver }} |
diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatable.js b/app/docker/components/datatables/volumes-datatable/volumesDatatable.js
index 1ad038cad..6b946438f 100644
--- a/app/docker/components/datatables/volumes-datatable/volumesDatatable.js
+++ b/app/docker/components/datatables/volumes-datatable/volumesDatatable.js
@@ -10,6 +10,7 @@ angular.module('portainer.docker').component('volumesDatatable', {
reverseOrder: '<',
showOwnershipColumn: '<',
showHostColumn: '<',
- removeAction: '<'
+ removeAction: '<',
+ showBrowseAction: '<'
}
});
diff --git a/app/docker/services/containerService.js b/app/docker/services/containerService.js
index d0368ade7..a0783d2d9 100644
--- a/app/docker/services/containerService.js
+++ b/app/docker/services/containerService.js
@@ -67,11 +67,7 @@ function ContainerServiceFactory($q, Container, ResourceControlService, LogHelpe
var deferred = $q.defer();
Container.create(configuration).$promise
.then(function success(data) {
- if (data.message) {
- deferred.reject({ msg: data.message });
- } else {
- deferred.resolve(data);
- }
+ deferred.resolve(data);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to create container', err: err });
diff --git a/app/docker/views/volumes/browse/browseVolumeController.js b/app/docker/views/volumes/browse/browseVolumeController.js
new file mode 100644
index 000000000..c8c503bba
--- /dev/null
+++ b/app/docker/views/volumes/browse/browseVolumeController.js
@@ -0,0 +1,11 @@
+angular.module('portainer.docker')
+.controller('BrowseVolumeController', ['$scope', '$transition$',
+function ($scope, $transition$) {
+
+ function initView() {
+ $scope.volumeId = $transition$.params().id;
+ $scope.nodeName = $transition$.params().nodeName;
+ }
+
+ initView();
+}]);
diff --git a/app/docker/views/volumes/browse/browsevolume.html b/app/docker/views/volumes/browse/browsevolume.html
new file mode 100644
index 000000000..b77beb87a
--- /dev/null
+++ b/app/docker/views/volumes/browse/browsevolume.html
@@ -0,0 +1,15 @@
+
+
+
+ Volumes > {{ volumeId }} > browse
+
+
+
+
diff --git a/app/docker/views/volumes/volumes.html b/app/docker/views/volumes/volumes.html
index c6ab31ab6..89376f0c3 100644
--- a/app/docker/views/volumes/volumes.html
+++ b/app/docker/views/volumes/volumes.html
@@ -10,12 +10,13 @@
diff --git a/app/portainer/components/datatables/endpoints-snapshot-datatable/endpointsSnapshotDatatable.html b/app/portainer/components/datatables/endpoints-snapshot-datatable/endpointsSnapshotDatatable.html
index 97ee7e809..a94ca0b37 100644
--- a/app/portainer/components/datatables/endpoints-snapshot-datatable/endpointsSnapshotDatatable.html
+++ b/app/portainer/components/datatables/endpoints-snapshot-datatable/endpointsSnapshotDatatable.html
@@ -52,7 +52,7 @@
-
+
{{ item.Name }}
|
@@ -63,7 +63,7 @@
- {{ item.Type | endpointtypename }}
+ {{ item.Type | endpointtypename }}
|
diff --git a/app/portainer/views/stacks/edit/stack.html b/app/portainer/views/stacks/edit/stack.html
index ca0508db9..1a8d3289b 100644
--- a/app/portainer/views/stacks/edit/stack.html
+++ b/app/portainer/views/stacks/edit/stack.html
@@ -171,7 +171,7 @@
|