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