From 48179b9e3d81c0fd661c5bd1652533da971c50c0 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 23 Jul 2018 07:01:03 +0200 Subject: [PATCH] feat(volume-browser): add the ability to browse volume content (#2051) --- app/__module.js | 1 + .../volume-browser-datatable.js | 15 +++ .../volumeBrowserDatatable.html | 90 ++++++++++++++ .../volume-browser/volume-browser.js | 8 ++ .../volume-browser/volumeBrowser.html | 5 + .../volume-browser/volumeBrowserController.js | 115 ++++++++++++++++++ app/agent/rest/agent.js | 2 +- app/agent/rest/browse.js | 22 ++++ app/agent/rest/response/browse.js | 9 ++ app/agent/services/volumeBrowserService.js | 27 ++++ app/docker/__module.js | 12 ++ .../volumes-datatable/volumesDatatable.html | 5 +- .../volumes-datatable/volumesDatatable.js | 3 +- app/docker/services/containerService.js | 6 +- .../volumes/browse/browseVolumeController.js | 11 ++ .../views/volumes/browse/browsevolume.html | 15 +++ app/docker/views/volumes/volumes.html | 13 +- .../endpointsSnapshotDatatable.html | 4 +- app/portainer/views/stacks/edit/stack.html | 4 +- package.json | 1 + vendor.yml | 1 + yarn.lock | 15 +++ 22 files changed, 366 insertions(+), 18 deletions(-) create mode 100644 app/agent/components/volume-browser/volume-browser-datatable/volume-browser-datatable.js create mode 100644 app/agent/components/volume-browser/volume-browser-datatable/volumeBrowserDatatable.html create mode 100644 app/agent/components/volume-browser/volume-browser.js create mode 100644 app/agent/components/volume-browser/volumeBrowser.html create mode 100644 app/agent/components/volume-browser/volumeBrowserController.js create mode 100644 app/agent/rest/browse.js create mode 100644 app/agent/rest/response/browse.js create mode 100644 app/agent/services/volumeBrowserService.js create mode 100644 app/docker/views/volumes/browse/browseVolumeController.js create mode 100644 app/docker/views/volumes/browse/browsevolume.html 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 @@ +
+ + +
+
+ {{ $ctrl.titleText }} +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + Size + + + + + + Last modification + + + + + Actions +
+ Go to parent +
+ + + + + + + {{ item.Name }} + + + {{ item.Name }} + + {{ item.Size | humansize }} + {{ item.ModTime | getisodatefromtimestamp }} + + + Download + + + Rename + + + Delete + +
Loading...
No files found.
+
+
+
+
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 @@