From f2deaee1107d3d32016997d58da4e235d7ccf1a9 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Sat, 20 Apr 2019 17:18:33 +0200 Subject: [PATCH] #592 feat(container-details): rename console to exec and add attach console --- app/docker/__module.js | 25 +++- .../containerQuickActions.html | 13 +- .../containersDatatable.html | 12 +- .../containersDatatableController.js | 3 +- .../serviceTasksDatatableController.js | 3 +- .../tasksDatatableController.js | 3 +- app/docker/rest/container.js | 4 + app/docker/services/attachService.js | 27 ++++ .../attach/containerAttachController.js | 133 ++++++++++++++++++ .../containers/attach/containerattach.html | 31 ++++ .../views/containers/edit/container.html | 3 +- .../containerExecController.js} | 26 ++-- .../containerexec.html} | 0 13 files changed, 258 insertions(+), 25 deletions(-) create mode 100644 app/docker/services/attachService.js create mode 100644 app/docker/views/containers/attach/containerAttachController.js create mode 100644 app/docker/views/containers/attach/containerattach.html rename app/docker/views/containers/{console/containerConsoleController.js => exec/containerExecController.js} (84%) rename app/docker/views/containers/{console/containerconsole.html => exec/containerexec.html} (100%) diff --git a/app/docker/__module.js b/app/docker/__module.js index bea1c8a66..78ff6a4ae 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -75,13 +75,25 @@ angular.module('portainer.docker', ['portainer.app']) } }; - var containerConsole = { - name: 'docker.containers.container.console', - url: '/console', + var containerExec = { + name: 'docker.containers.container.exec', + url: '/exec', views: { 'content@': { - templateUrl: './views/containers/console/containerconsole.html', - controller: 'ContainerConsoleController' + templateUrl: './views/containers/exec/containerexec.html', + controller: 'ContainerExecController' + } + } + }; + + + var containerAttach = { + name: 'docker.containers.container.attach', + url: '/attach', + views: { + 'content@': { + templateUrl: './views/containers/attach/containerattach.html', + controller: 'ContainerAttachController' } } }; @@ -473,7 +485,8 @@ angular.module('portainer.docker', ['portainer.app']) $stateRegistryProvider.register(configCreation); $stateRegistryProvider.register(containers); $stateRegistryProvider.register(container); - $stateRegistryProvider.register(containerConsole); + $stateRegistryProvider.register(containerExec); + $stateRegistryProvider.register(containerAttach); $stateRegistryProvider.register(containerCreation); $stateRegistryProvider.register(containerInspect); $stateRegistryProvider.register(containerLogs); diff --git a/app/docker/components/container-quick-actions/containerQuickActions.html b/app/docker/components/container-quick-actions/containerQuickActions.html index 5475dd9c5..17cbdc7d6 100644 --- a/app/docker/components/container-quick-actions/containerQuickActions.html +++ b/app/docker/components/container-quick-actions/containerQuickActions.html @@ -35,10 +35,17 @@ + ui-sref="docker.containers.container.exec({id: $ctrl.containerId, nodeName: $ctrl.nodeName})" + title="Execute Command"> + + + \ No newline at end of file diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.html b/app/docker/components/datatables/containers-datatable/containersDatatable.html index e9179cd33..0d71ddc84 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.html +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.html @@ -83,8 +83,12 @@
- - + + +
+
+ +
@@ -153,7 +157,7 @@
- + Quick actions @@ -221,7 +225,7 @@ {{ item.Status }} {{ item.Status }} - + diff --git a/app/docker/components/datatables/containers-datatable/containersDatatableController.js b/app/docker/components/datatables/containers-datatable/containersDatatableController.js index 09f75e0a9..3c1048e58 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatableController.js +++ b/app/docker/components/datatables/containers-datatable/containersDatatableController.js @@ -24,7 +24,8 @@ function (PaginationService, DatatableService, EndpointProvider) { containerNameTruncateSize: 32, showQuickActionStats: true, showQuickActionLogs: true, - showQuickActionConsole: true, + showQuickActionExec: true, + showQuickActionAttach: true, showQuickActionInspect: true }; diff --git a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js index 7a7ba1763..780df2bf6 100644 --- a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js +++ b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js @@ -9,7 +9,8 @@ function (DatatableService) { orderBy: this.orderBy, showQuickActionStats: true, showQuickActionLogs: true, - showQuickActionConsole: true, + showQuickActionExec: true, + showQuickActionAttach: true, showQuickActionInspect: true }; diff --git a/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js b/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js index 368fa76d2..5d5ff71be 100644 --- a/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js +++ b/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js @@ -4,8 +4,9 @@ function (PaginationService, DatatableService) { this.state = { showQuickActionStats: true, showQuickActionLogs: true, - showQuickActionConsole: true, + showQuickActionExec: true, showQuickActionInspect: true, + showQuickActionAttach: true, selectAll: false, orderBy: this.orderBy, paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey), diff --git a/app/docker/rest/container.js b/app/docker/rest/container.js index 7841ba2b2..94c3befbc 100644 --- a/app/docker/rest/container.js +++ b/app/docker/rest/container.js @@ -73,6 +73,10 @@ function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, C }, prune: { method: 'POST', params: { action: 'prune', filters: '@filters' } + }, + resize: { + method: 'POST', params: {id: '@id', action: 'resize', h: '@height', w: '@width'}, + transformResponse: genericHandler, ignoreLoadingBar: true } }); }]); diff --git a/app/docker/services/attachService.js b/app/docker/services/attachService.js new file mode 100644 index 000000000..9d5647878 --- /dev/null +++ b/app/docker/services/attachService.js @@ -0,0 +1,27 @@ +angular.module('portainer.docker') +.factory('AttachService', ['$q', '$timeout', 'Container', function AttachServiceFactory($q, $timeout, Container) { + 'use strict'; + var service = {}; + + service.resizeTTY = function(execId, height, width, timeout) { + var deferred = $q.defer(); + + $timeout(function() { + Container.resize({}, { id: execId, height: height, width: width }).$promise + .then(function success(data) { + if (data.message) { + deferred.reject({ msg: 'Unable to attach to container', err: data.message }); + } else { + deferred.resolve(data); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to attach to container', err: err }); + }); + }, timeout); + + return deferred.promise; + }; + + return service; +}]); diff --git a/app/docker/views/containers/attach/containerAttachController.js b/app/docker/views/containers/attach/containerAttachController.js new file mode 100644 index 000000000..693efd28d --- /dev/null +++ b/app/docker/views/containers/attach/containerAttachController.js @@ -0,0 +1,133 @@ +import { Terminal } from 'xterm'; + +angular.module('portainer.docker') +.controller('ContainerAttachController', ['$scope', '$transition$', 'ContainerService', 'ImageService', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'AttachService', 'HttpRequestHelper', 'LocalStorage', +function ($scope, $transition$, ContainerService, ImageService, EndpointProvider, Notifications, ContainerHelper, AttachService, HttpRequestHelper, LocalStorage) { + var socket, term; + + $scope.state = { + loaded: false, + connected: false, + connecting: false + }; + + // Ensure the socket is closed before leaving the view + $scope.$on('$stateChangeStart', function () { + if (socket && socket !== null) { + socket.close(); + } + }); + + $scope.connect = function() { + if ($scope.state.connecting || $scope.state.connected) { + return; + } + + $scope.state.connecting = true; + + var termWidth = Math.floor(($('#terminal-container').width() - 20) / 8.39); + var termHeight = 30; + + var attachId = $transition$.params().id; + var jwtToken = LocalStorage.getJWT(); + + + ContainerService.container(attachId).then((details)=> { + + if (!details.State.Running) { + Notifications.error("Failure", details, "Container is not running!"); + return; + } + + let params = { + token: jwtToken, + endpointId: EndpointProvider.endpointID(), + id: attachId + }; + + let param_string = Object.keys(params).map((k) => k + "=" + params[k]).join("&"); + + var url = window.location.href.split('#')[0] + 'api/websocket/attach?' + param_string; + + if ($transition$.params().nodeName) { + url += '&nodeName=' + $transition$.params().nodeName; + } + if (url.indexOf('https') > -1) { + url = url.replace('https://', 'wss://'); + } else { + url = url.replace('http://', 'ws://'); + } + initTerm(url, termHeight, termWidth); + return AttachService.resizeTTY(attachId, termHeight, termWidth, 2000); + }) + .catch(function error(err) { + Notifications.error('Error', err, 'Unable to retrieve container details'); + }); + }; + + $scope.disconnect = function() { + if (socket !== null) { + socket.close(); + } + }; + + function initTerm(url, height, width) { + socket = new WebSocket(url); + + socket.onopen = function() { + $scope.state.connected = true; + $scope.state.connecting = false; + term = new Terminal(); + + term.on('data', function (data) { + socket.send(data); + }); + term.open(document.getElementById('terminal-container')); + term.focus(); + term.resize(width, height); + term.setOption('cursorBlink', true); + term.fit(); + + window.onresize = function() { + term.fit(); + }; + + socket.onmessage = function (e) { + term.write(e.data); + }; + + socket.onerror = function (err) { + $scope.disconnect(); + Notifications.error("Failure",err, "Connection error"); + }; + + socket.onclose = function() { + $scope.state.connected = false; + $scope.state.connecting = false; + term.write("\n\r(connection closed)"); + if (term !== null) { + term.dispose(); + } + }; + }; + } + + function initView() { + HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName); + ContainerService.container($transition$.params().id) + .then(function success(data) { + var container = data; + $scope.container = container; + return ImageService.image(container.Image); + }) + .then(function success() { + $scope.state.loaded = true; + $scope.connect(); + }) + .catch(function error(err) { + Notifications.error('Error', err, 'Unable to retrieve container details'); + }); + } + + initView(); +}]); diff --git a/app/docker/views/containers/attach/containerattach.html b/app/docker/views/containers/attach/containerattach.html new file mode 100644 index 000000000..d3bc8ea0f --- /dev/null +++ b/app/docker/views/containers/attach/containerattach.html @@ -0,0 +1,31 @@ + + + + Containers > {{ container.Name|trimcontainername }} > Console + + + +
+
+ + + +
+
+ + +
+
+
+
+
+
+ +
+
+
+
+
diff --git a/app/docker/views/containers/edit/container.html b/app/docker/views/containers/edit/container.html index f9160fa51..5879f7755 100644 --- a/app/docker/views/containers/edit/container.html +++ b/app/docker/views/containers/edit/container.html @@ -87,7 +87,8 @@ Logs Inspect Stats - Console + Execute + Attach diff --git a/app/docker/views/containers/console/containerConsoleController.js b/app/docker/views/containers/exec/containerExecController.js similarity index 84% rename from app/docker/views/containers/console/containerConsoleController.js rename to app/docker/views/containers/exec/containerExecController.js index f1bf02821..667b0f197 100644 --- a/app/docker/views/containers/console/containerConsoleController.js +++ b/app/docker/views/containers/exec/containerExecController.js @@ -1,13 +1,14 @@ import { Terminal } from 'xterm'; angular.module('portainer.docker') -.controller('ContainerConsoleController', ['$scope', '$transition$', 'ContainerService', 'ImageService', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ExecService', 'HttpRequestHelper', 'LocalStorage', 'CONSOLE_COMMANDS_LABEL_PREFIX', +.controller('ContainerExecController', ['$scope', '$transition$', 'ContainerService', 'ImageService', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ExecService', 'HttpRequestHelper', 'LocalStorage', 'CONSOLE_COMMANDS_LABEL_PREFIX', function ($scope, $transition$, ContainerService, ImageService, EndpointProvider, Notifications, ContainerHelper, ExecService, HttpRequestHelper, LocalStorage, CONSOLE_COMMANDS_LABEL_PREFIX) { var socket, term; $scope.state = { loaded: false, - connected: false + connected: false, + connecting: false }; $scope.formValues = {}; @@ -21,6 +22,12 @@ function ($scope, $transition$, ContainerService, ImageService, EndpointProvider }); $scope.connect = function() { + if ($scope.state.connecting || $scope.state.connected) { + return; + } + + $scope.state.connecting = true; + var termWidth = Math.floor(($('#terminal-container').width() - 20) / 8.39); var termHeight = 30; var command = $scope.formValues.isCustomCommand ? @@ -59,20 +66,17 @@ function ($scope, $transition$, ContainerService, ImageService, EndpointProvider }; $scope.disconnect = function() { - $scope.state.connected = false; if (socket !== null) { socket.close(); } - if (term !== null) { - term.destroy(); - } }; function initTerm(url, height, width) { socket = new WebSocket(url); - $scope.state.connected = true; socket.onopen = function() { + $scope.state.connected = true; + $scope.state.connecting = false; term = new Terminal(); term.on('data', function (data) { @@ -92,10 +96,16 @@ function ($scope, $transition$, ContainerService, ImageService, EndpointProvider term.write(e.data); }; socket.onerror = function () { - $scope.state.connected = false; + $scope.disconnect(); + term.write("\n\r(connection error)"); }; socket.onclose = function() { $scope.state.connected = false; + $scope.state.connecting = false; + term.write("\n\r(connection closed)"); + if (term !== null) { + term.dispose(); + } }; }; } diff --git a/app/docker/views/containers/console/containerconsole.html b/app/docker/views/containers/exec/containerexec.html similarity index 100% rename from app/docker/views/containers/console/containerconsole.html rename to app/docker/views/containers/exec/containerexec.html