diff --git a/app/agent/components/file-uploader/file-uploader-controller.js b/app/agent/components/file-uploader/file-uploader-controller.js new file mode 100644 index 000000000..e6516c67c --- /dev/null +++ b/app/agent/components/file-uploader/file-uploader-controller.js @@ -0,0 +1,23 @@ +angular.module('portainer.agent').controller('FileUploaderController', [ + '$q', + function FileUploaderController($q) { + var ctrl = this; + + ctrl.state = { + uploadInProgress: false + }; + + ctrl.onFileSelected = onFileSelected; + + function onFileSelected(file) { + if (!file) { + return; + } + + ctrl.state.uploadInProgress = true; + $q.when(ctrl.uploadFile(file)).finally(function toggleProgress() { + ctrl.state.uploadInProgress = false; + }); + } + } +]); diff --git a/app/agent/components/file-uploader/file-uploader.html b/app/agent/components/file-uploader/file-uploader.html new file mode 100644 index 000000000..e092ce5d6 --- /dev/null +++ b/app/agent/components/file-uploader/file-uploader.html @@ -0,0 +1,6 @@ + diff --git a/app/agent/components/file-uploader/file-uploader.js b/app/agent/components/file-uploader/file-uploader.js new file mode 100644 index 000000000..58ba7b04b --- /dev/null +++ b/app/agent/components/file-uploader/file-uploader.js @@ -0,0 +1,7 @@ +angular.module('portainer.agent').component('fileUploader', { + templateUrl: 'app/agent/components/file-uploader/file-uploader.html', + controller: 'FileUploaderController', + bindings: { + uploadFile: ' + + + + -
-
- {{ $ctrl.titleText }} -
-
@@ -41,23 +41,29 @@ - + @@ -65,13 +71,14 @@ {{ item.ModTime | getisodatefromtimestamp }} @@ -87,4 +94,4 @@ - + \ No newline at end of file diff --git a/app/agent/components/files-datatable/files-datatable.js b/app/agent/components/files-datatable/files-datatable.js new file mode 100644 index 000000000..4e90e1d00 --- /dev/null +++ b/app/agent/components/files-datatable/files-datatable.js @@ -0,0 +1,22 @@ +angular.module('portainer.agent').component('filesDatatable', { + templateUrl: 'app/agent/components/files-datatable/files-datatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + + isRoot: '<', + goToParent: '&', + browse: '&', + rename: '&', + download: '&', + delete: '&', + + isUploadAllowed: '<', + onFileSelectedForUpload: '<' + } +}); diff --git a/app/agent/components/host-browser/host-browser-controller.js b/app/agent/components/host-browser/host-browser-controller.js new file mode 100644 index 000000000..a50cb6b6e --- /dev/null +++ b/app/agent/components/host-browser/host-browser-controller.js @@ -0,0 +1,147 @@ +angular.module('portainer.agent').controller('HostBrowserController', [ + 'HostBrowserService', 'Notifications', 'FileSaver', 'ModalService', + function HostBrowserController(HostBrowserService, Notifications, FileSaver, ModalService) { + var ctrl = this; + var ROOT_PATH = '/host'; + ctrl.state = { + path: ROOT_PATH + }; + + ctrl.goToParent = goToParent; + ctrl.browse = browse; + ctrl.renameFile = renameFile; + ctrl.downloadFile = downloadFile; + ctrl.deleteFile = confirmDeleteFile; + ctrl.isRoot = isRoot; + ctrl.onFileSelectedForUpload = onFileSelectedForUpload; + ctrl.$onInit = $onInit; + ctrl.getRelativePath = getRelativePath; + + function getRelativePath(path) { + path = path || ctrl.state.path; + var rootPathRegex = new RegExp('^' + ROOT_PATH + '\/?'); + var relativePath = path.replace(rootPathRegex, '/'); + return relativePath; + } + + function goToParent() { + getFilesForPath(parentPath(this.state.path)); + } + + function isRoot() { + return ctrl.state.path === ROOT_PATH; + } + + function browse(folder) { + getFilesForPath(buildPath(ctrl.state.path, folder)); + } + + function getFilesForPath(path) { + HostBrowserService.ls(path) + .then(function onFilesLoaded(files) { + ctrl.state.path = path; + ctrl.files = files; + }) + .catch(function onLoadingFailed(err) { + Notifications.error('Failure', err, 'Unable to browse'); + }); + } + + function renameFile(name, newName) { + var filePath = buildPath(ctrl.state.path, name); + var newFilePath = buildPath(ctrl.state.path, newName); + + HostBrowserService.rename(filePath, newFilePath) + .then(function onRenameSuccess() { + Notifications.success('File successfully renamed', getRelativePath(newFilePath)); + return HostBrowserService.ls(ctrl.state.path); + }) + .then(function onFilesLoaded(files) { + ctrl.files = files; + }) + .catch(function notifyOnError(err) { + Notifications.error('Failure', err, 'Unable to rename file'); + }); + } + + function downloadFile(file) { + var filePath = buildPath(ctrl.state.path, file); + HostBrowserService.get(filePath) + .then(function onFileReceived(data) { + var downloadData = new Blob([data.file], { + type: 'text/plain;charset=utf-8' + }); + FileSaver.saveAs(downloadData, file); + }) + .catch(function notifyOnError(err) { + Notifications.error('Failure', err, 'Unable to download file'); + }); + } + + function confirmDeleteFile(name) { + var filePath = buildPath(ctrl.state.path, name); + + ModalService.confirmDeletion( + 'Are you sure that you want to delete ' + getRelativePath(filePath) + ' ?', + function onConfirm(confirmed) { + if (!confirmed) { + return; + } + return deleteFile(filePath); + } + ); + } + + function deleteFile(path) { + HostBrowserService.delete(path) + .then(function onDeleteSuccess() { + Notifications.success('File successfully deleted', getRelativePath(path)); + return HostBrowserService.ls(ctrl.state.path); + }) + .then(function onFilesLoaded(data) { + ctrl.files = data; + }) + .catch(function notifyOnError(err) { + Notifications.error('Failure', err, 'Unable to delete file'); + }); + } + + function $onInit() { + getFilesForPath(ROOT_PATH); + } + + function parentPath(path) { + if (path === ROOT_PATH) { + return ROOT_PATH; + } + + var split = _.split(path, '/'); + return _.join(_.slice(split, 0, split.length - 1), '/'); + } + + function buildPath(parent, file) { + if (parent.lastIndexOf('/') === parent.length - 1) { + return parent + file; + } + return parent + '/' + file; + } + + function onFileSelectedForUpload(file) { + HostBrowserService.upload(ctrl.state.path, file) + .then(function onFileUpload() { + onFileUploaded(); + }) + .catch(function onFileUpload(err) { + Notifications.error('Failure', err, 'Unable to upload file'); + }); + } + + function onFileUploaded() { + refreshList(); + } + + function refreshList() { + getFilesForPath(ctrl.state.path); + } + } +]); diff --git a/app/agent/components/host-browser/host-browser.html b/app/agent/components/host-browser/host-browser.html new file mode 100644 index 000000000..cf7b59306 --- /dev/null +++ b/app/agent/components/host-browser/host-browser.html @@ -0,0 +1,16 @@ + + + diff --git a/app/agent/components/host-browser/host-browser.js b/app/agent/components/host-browser/host-browser.js new file mode 100644 index 000000000..a136730ee --- /dev/null +++ b/app/agent/components/host-browser/host-browser.js @@ -0,0 +1,5 @@ +angular.module('portainer.agent').component('hostBrowser', { + controller: 'HostBrowserController', + templateUrl: 'app/agent/components/host-browser/host-browser.html', + bindings: {} +}); 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 deleted file mode 100644 index e3139974d..000000000 --- a/app/agent/components/volume-browser/volume-browser-datatable/volume-browser-datatable.js +++ /dev/null @@ -1,15 +0,0 @@ -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/volumeBrowser.html b/app/agent/components/volume-browser/volumeBrowser.html index 643d8c88b..3759f22d2 100644 --- a/app/agent/components/volume-browser/volumeBrowser.html +++ b/app/agent/components/volume-browser/volumeBrowser.html @@ -1,5 +1,11 @@ - + is-root="$ctrl.state.path === '/'" + go-to-parent="$ctrl.up()" + browse="$ctrl.browse(name)" + rename="$ctrl.rename(name, newName)" + download="$ctrl.download(name)" + delete="$ctrl.delete(name)" +> diff --git a/app/agent/services/hostBrowserService.js b/app/agent/services/hostBrowserService.js new file mode 100644 index 000000000..935d34be4 --- /dev/null +++ b/app/agent/services/hostBrowserService.js @@ -0,0 +1,44 @@ +angular.module('portainer.agent').factory('HostBrowserService', [ + 'Browse', 'Upload', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', '$q', + function HostBrowserServiceFactory(Browse, Upload, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q) { + var service = {}; + + service.ls = ls; + service.get = get; + service.delete = deletePath; + service.rename = rename; + service.upload = upload; + + function ls(path) { + return Browse.ls({ path: path }).$promise; + } + + function get(path) { + return Browse.get({ path: path }).$promise; + } + + function deletePath(path) { + return Browse.delete({ path: path }).$promise; + } + + function rename(path, newPath) { + var payload = { + CurrentFilePath: path, + NewFilePath: newPath + }; + return Browse.rename({}, payload).$promise; + } + + function upload(path, file, onProgress) { + var deferred = $q.defer(); + var url = API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/browse/put'; + Upload.upload({ + url: url, + data: { file: file, Path: path } + }).then(deferred.resolve, deferred.reject, onProgress); + return deferred.promise; + } + + return service; + } +]); diff --git a/app/agent/services/volumeBrowserService.js b/app/agent/services/volumeBrowserService.js index 2a5c92f56..1da020c38 100644 --- a/app/agent/services/volumeBrowserService.js +++ b/app/agent/services/volumeBrowserService.js @@ -1,27 +1,29 @@ -angular.module('portainer.agent') -.factory('VolumeBrowserService', ['$q', 'Browse', function VolumeBrowserServiceFactory($q, Browse) { - 'use strict'; - var service = {}; +angular.module('portainer.agent').factory('VolumeBrowserService', [ + '$q', 'Browse', + function VolumeBrowserServiceFactory($q, Browse) { + 'use strict'; + var service = {}; - service.ls = function(volumeId, path) { - return Browse.ls({ volumeID: volumeId, path: path }).$promise; - }; - - service.get = function(volumeId, path) { - return Browse.get({ volumeID: volumeId, path: path }).$promise; - }; - - service.delete = function(volumeId, path) { - return Browse.delete({ volumeID: volumeId, path: path }).$promise; - }; - - service.rename = function(volumeId, path, newPath) { - var payload = { - CurrentFilePath: path, - NewFilePath: newPath + service.ls = function(volumeId, path) { + return Browse.ls({ volumeID: volumeId, path: path }).$promise; }; - return Browse.rename({ volumeID: volumeId }, payload).$promise; - }; - return service; -}]); + service.get = function(volumeId, path) { + return Browse.get({ volumeID: volumeId, path: path }).$promise; + }; + + service.delete = function(volumeId, path) { + return Browse.delete({ volumeID: volumeId, path: path }).$promise; + }; + + service.rename = function(volumeId, path, newPath) { + var payload = { + CurrentFilePath: path, + NewFilePath: newPath + }; + return Browse.rename({ volumeID: volumeId }, payload).$promise; + }; + + return service; + } +]); diff --git a/app/docker/__module.js b/app/docker/__module.js index 23ce18433..4b4bdfc74 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -139,6 +139,16 @@ angular.module('portainer.docker', ['portainer.app']) } }; + var hostBrowser = { + name: 'docker.host.browser', + url: '/browser', + views: { + 'content@': { + component: 'hostBrowserView' + } + } + }; + var events = { name: 'docker.events', url: '/events', @@ -243,6 +253,16 @@ angular.module('portainer.docker', ['portainer.app']) } }; + var nodeBrowser = { + name: 'docker.nodes.node.browse', + url: '/browse', + views: { + 'content@': { + component: 'nodeBrowserView' + } + } + }; + var secrets = { name: 'docker.secrets', url: '/secrets', @@ -414,6 +434,8 @@ angular.module('portainer.docker', ['portainer.app']) } }; + + $stateRegistryProvider.register(configs); $stateRegistryProvider.register(config); $stateRegistryProvider.register(configCreation); @@ -427,6 +449,7 @@ angular.module('portainer.docker', ['portainer.app']) $stateRegistryProvider.register(docker); $stateRegistryProvider.register(dashboard); $stateRegistryProvider.register(host); + $stateRegistryProvider.register(hostBrowser); $stateRegistryProvider.register(events); $stateRegistryProvider.register(images); $stateRegistryProvider.register(image); @@ -437,6 +460,7 @@ angular.module('portainer.docker', ['portainer.app']) $stateRegistryProvider.register(networkCreation); $stateRegistryProvider.register(nodes); $stateRegistryProvider.register(node); + $stateRegistryProvider.register(nodeBrowser); $stateRegistryProvider.register(secrets); $stateRegistryProvider.register(secret); $stateRegistryProvider.register(secretCreation); diff --git a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js b/app/docker/components/dockerSidebarContent/docker-sidebar-content.js index 325a3497d..5031a44a7 100644 --- a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js +++ b/app/docker/components/dockerSidebarContent/docker-sidebar-content.js @@ -1,9 +1,9 @@ angular.module('portainer.docker').component('dockerSidebarContent', { templateUrl: 'app/docker/components/dockerSidebarContent/dockerSidebarContent.html', bindings: { - 'endpointApiVersion': '<', - 'swarmManagement': '<', - 'standaloneManagement': '<', - 'adminAccess': '<' + endpointApiVersion: '<', + swarmManagement: '<', + standaloneManagement: '<', + adminAccess: '<' } }); diff --git a/app/docker/components/host-overview/host-overview.html b/app/docker/components/host-overview/host-overview.html index baa43628e..81ac13c62 100644 --- a/app/docker/components/host-overview/host-overview.html +++ b/app/docker/components/host-overview/host-overview.html @@ -1,13 +1,17 @@ - + Docker - + diff --git a/app/docker/components/host-overview/host-overview.js b/app/docker/components/host-overview/host-overview.js index add9515b4..78883e4e8 100644 --- a/app/docker/components/host-overview/host-overview.js +++ b/app/docker/components/host-overview/host-overview.js @@ -6,7 +6,8 @@ angular.module('portainer.docker').component('hostOverview', { devices: '<', disks: '<', isAgent: '<', - refreshUrl: '@' + refreshUrl: '@', + browseUrl: '@' }, transclude: true }); diff --git a/app/docker/components/host-view-panels/host-details-panel/host-details-panel.html b/app/docker/components/host-view-panels/host-details-panel/host-details-panel.html index 2ea05f88f..9bb9b7f85 100644 --- a/app/docker/components/host-view-panels/host-details-panel/host-details-panel.html +++ b/app/docker/components/host-view-panels/host-details-panel/host-details-panel.html @@ -26,6 +26,17 @@ + + + +
- Go to parent + Go + to parent
- + - + - {{ item.Name }} + {{ item.Name }} - {{ item.Name }} + {{ + item.Name }} {{ item.Size | humansize }} - + Download Rename - + Delete Total memory {{ $ctrl.host.totalMemory | humansize }}
+ +
diff --git a/app/docker/components/host-view-panels/host-details-panel/host-details-panel.js b/app/docker/components/host-view-panels/host-details-panel/host-details-panel.js index 6865b5872..328832c42 100644 --- a/app/docker/components/host-view-panels/host-details-panel/host-details-panel.js +++ b/app/docker/components/host-view-panels/host-details-panel/host-details-panel.js @@ -3,6 +3,7 @@ angular.module('portainer.docker').component('hostDetailsPanel', { 'app/docker/components/host-view-panels/host-details-panel/host-details-panel.html', bindings: { host: '<', - isAgent: '<' + isAgent: '<', + browseUrl: '@' } }); diff --git a/app/docker/views/host/host-browser-view/host-browser-view-controller.js b/app/docker/views/host/host-browser-view/host-browser-view-controller.js new file mode 100644 index 000000000..9638e966e --- /dev/null +++ b/app/docker/views/host/host-browser-view/host-browser-view-controller.js @@ -0,0 +1,21 @@ +angular + .module('portainer.docker') + .controller('HostBrowserViewController', [ + 'SystemService', 'HttpRequestHelper', + function HostBrowserViewController(SystemService, HttpRequestHelper) { + var ctrl = this; + + ctrl.$onInit = $onInit; + + function $onInit() { + loadInfo(); + } + + function loadInfo() { + SystemService.info().then(function onInfoLoaded(host) { + HttpRequestHelper.setPortainerAgentTargetHeader(host.Name); + ctrl.host = host; + }); + } + } + ]); diff --git a/app/docker/views/host/host-browser-view/host-browser-view.html b/app/docker/views/host/host-browser-view/host-browser-view.html new file mode 100644 index 000000000..2d87e4b2e --- /dev/null +++ b/app/docker/views/host/host-browser-view/host-browser-view.html @@ -0,0 +1,14 @@ + + + + Host > {{ $ctrl.host.Name }} > browse + + + +
+
+ +
+
diff --git a/app/docker/views/host/host-browser-view/host-browser-view.js b/app/docker/views/host/host-browser-view/host-browser-view.js new file mode 100644 index 000000000..7ce93994a --- /dev/null +++ b/app/docker/views/host/host-browser-view/host-browser-view.js @@ -0,0 +1,4 @@ +angular.module('portainer.docker').component('hostBrowserView', { + templateUrl: 'app/docker/views/host/host-browser-view/host-browser-view.html', + controller: 'HostBrowserViewController' +}); diff --git a/app/docker/views/host/host-view.html b/app/docker/views/host/host-view.html index 419e96995..ada86fef8 100644 --- a/app/docker/views/host/host-view.html +++ b/app/docker/views/host/host-view.html @@ -1,8 +1,10 @@ \ No newline at end of file diff --git a/app/docker/views/nodes/node-browser/node-browser-controller.js b/app/docker/views/nodes/node-browser/node-browser-controller.js new file mode 100644 index 000000000..d846ac8f9 --- /dev/null +++ b/app/docker/views/nodes/node-browser/node-browser-controller.js @@ -0,0 +1,20 @@ +angular.module('portainer.docker').controller('NodeBrowserController', [ + 'NodeService', 'HttpRequestHelper', '$stateParams', + function NodeBrowserController(NodeService, HttpRequestHelper, $stateParams) { + var ctrl = this; + + ctrl.$onInit = $onInit; + + function $onInit() { + ctrl.nodeId = $stateParams.id; + loadNode(); + } + + function loadNode() { + NodeService.node(ctrl.nodeId).then(function onNodeLoaded(node) { + HttpRequestHelper.setPortainerAgentTargetHeader(node.Hostname); + ctrl.node = node; + }); + } + } +]); diff --git a/app/docker/views/nodes/node-browser/node-browser.html b/app/docker/views/nodes/node-browser/node-browser.html new file mode 100644 index 000000000..2edeae199 --- /dev/null +++ b/app/docker/views/nodes/node-browser/node-browser.html @@ -0,0 +1,14 @@ + + + + Swarm > {{ $ctrl.node.Hostname }} > browse + + + +
+
+ +
+
diff --git a/app/docker/views/nodes/node-browser/node-browser.js b/app/docker/views/nodes/node-browser/node-browser.js new file mode 100644 index 000000000..3e7269384 --- /dev/null +++ b/app/docker/views/nodes/node-browser/node-browser.js @@ -0,0 +1,4 @@ +angular.module('portainer.docker').component('nodeBrowserView', { + templateUrl: 'app/docker/views/nodes/node-browser/node-browser.html', + controller: 'NodeBrowserController' +}); diff --git a/app/docker/views/nodes/node-details/node-details-view.html b/app/docker/views/nodes/node-details/node-details-view.html index 13df99558..17d0dc470 100644 --- a/app/docker/views/nodes/node-details/node-details-view.html +++ b/app/docker/views/nodes/node-details/node-details-view.html @@ -2,9 +2,11 @@ is-agent="$ctrl.state.isAgent" host-details="$ctrl.hostDetails" engine-details="$ctrl.engineDetails" - refresh-url="docker.nodes.node" disks="$ctrl.disks" devices="$ctrl.devices" + + refresh-url="docker.nodes.node" + browse-url="docker.nodes.node.browse" >