#592 feat(container-details): rename console to exec and add attach console

pull/2842/head
Tilman Vatteroth 2019-04-20 17:18:33 +02:00
parent ff59f2d85f
commit f2deaee110
13 changed files with 258 additions and 25 deletions

View File

@ -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);

View File

@ -35,10 +35,17 @@
<i class="fa fa-chart-area space-right" aria-hidden="true"></i>
</a>
<a
ng-if="$ctrl.state.showQuickActionConsole && ['starting', 'running', 'healthy', 'unhealthy'].indexOf($ctrl.status) !== -1 && $ctrl.taskId === undefined"
ng-if="$ctrl.state.showQuickActionExec && ['starting', 'running', 'healthy', 'unhealthy'].indexOf($ctrl.status) !== -1 && $ctrl.taskId === undefined"
style="margin: 0 2.5px;"
ui-sref="docker.containers.container.console({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
title="Console">
ui-sref="docker.containers.container.exec({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
title="Execute Command">
<i class="fa fa-terminal space-right" aria-hidden="true"></i>
</a>
<a
ng-if="$ctrl.state.showQuickActionAttach && ['starting', 'running', 'healthy', 'unhealthy'].indexOf($ctrl.status) !== -1 && $ctrl.taskId === undefined"
style="margin: 0 2.5px;"
ui-sref="docker.containers.container.attach({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
title="Attach">
<i class="fa fa-plug space-right" aria-hidden="true"></i>
</a>
</div>

View File

@ -83,8 +83,12 @@
<label for="setting_show_logs">Logs</label>
</div>
<div class="md-checkbox">
<input id="setting_show_console" type="checkbox" ng-model="$ctrl.settings.showQuickActionConsole" ng-change="$ctrl.onSettingsQuickActionChange()"/>
<label for="setting_show_console">Console</label>
<input id="setting_show_exec" type="checkbox" ng-model="$ctrl.settings.showQuickActionExec" ng-change="$ctrl.onSettingsQuickActionChange()"/>
<label for="setting_show_exec">Exec</label>
</div>
<div class="md-checkbox">
<input id="setting_show_attach" type="checkbox" ng-model="$ctrl.settings.showQuickActionAttach" ng-change="$ctrl.onSettingsQuickActionChange()"/>
<label for="setting_show_attach">Attach</label>
</div>
<div class="md-checkbox">
<input id="setting_show_inspect" type="checkbox" ng-model="$ctrl.settings.showQuickActionInspect" ng-change="$ctrl.onSettingsQuickActionChange()"/>
@ -153,7 +157,7 @@
</div>
</div>
</th>
<th ng-if="$ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect" ng-show="$ctrl.columnVisibility.columns.actions.display">
<th ng-if="$ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionExec || $ctrl.settings.showQuickActionAttach || $ctrl.settings.showQuickActionInspect" ng-show="$ctrl.columnVisibility.columns.actions.display">
Quick actions
</th>
<th ng-show="$ctrl.columnVisibility.columns.stack.display">
@ -221,7 +225,7 @@
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) !== -1" class="label label-{{ item.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ item.Status }}</span>
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) === -1" class="label label-{{ item.Status|containerstatusbadge }}">{{ item.Status }}</span>
</td>
<td ng-if="!$ctrl.offlineMode && ($ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect)" ng-show="$ctrl.columnVisibility.columns.actions.display">
<td ng-if="!$ctrl.offlineMode && ($ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionExec || $ctrl.settings.showQuickActionAttach || $ctrl.settings.showQuickActionInspect)" ng-show="$ctrl.columnVisibility.columns.actions.display">
<container-quick-actions container-id="item.Id" node-name="item.NodeName" status="item.Status" state="$ctrl.settings"></container-quick-actions>
</td>
<td ng-if="$ctrl.offlineMode">

View File

@ -24,7 +24,8 @@ function (PaginationService, DatatableService, EndpointProvider) {
containerNameTruncateSize: 32,
showQuickActionStats: true,
showQuickActionLogs: true,
showQuickActionConsole: true,
showQuickActionExec: true,
showQuickActionAttach: true,
showQuickActionInspect: true
};

View File

@ -9,7 +9,8 @@ function (DatatableService) {
orderBy: this.orderBy,
showQuickActionStats: true,
showQuickActionLogs: true,
showQuickActionConsole: true,
showQuickActionExec: true,
showQuickActionAttach: true,
showQuickActionInspect: true
};

View File

@ -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),

View File

@ -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
}
});
}]);

View File

@ -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;
}]);

View File

@ -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();
}]);

View File

@ -0,0 +1,31 @@
<rd-header>
<rd-header-title title-text="Attach to Container"></rd-header-title>
<rd-header-content ng-if="state.loaded">
<a ui-sref="docker.containers">Containers</a> &gt; <a ui-sref="docker.containers.container({id: container.Id})">{{ container.Name|trimcontainername }}</a> &gt; Console
</rd-header-content>
</rd-header>
<div class="row" ng-if="state.loaded">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plug" title-text="Attach"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div>
<button type="button" class="btn btn-default" ng-if="state.connected" ng-click="disconnect()">Disattach</button>
<button type="button" class="btn btn-default" ng-if="!state.connected" ng-disabled="state.connecting" ng-click="connect()">
<span ng-hide="state.connecting">Attach</span>
<span ng-show="state.connecting">Attaching...</span>
</button>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<div id="terminal-container" class="terminal-container"></div>
</div>
</div>

View File

@ -87,7 +87,8 @@
<a class="btn" type="button" ui-sref="docker.containers.container.logs({ id: container.Id })"><i class="fa fa-file-alt space-right" aria-hidden="true"></i>Logs</a>
<a class="btn" type="button" ui-sref="docker.containers.container.inspect({ id: container.Id })"><i class="fa fa-info-circle space-right" aria-hidden="true"></i>Inspect</a>
<a class="btn" type="button" ui-sref="docker.containers.container.stats({ id: container.Id })"><i class="fa fa-chart-area space-right" aria-hidden="true"></i>Stats</a>
<a class="btn" type="button" ui-sref="docker.containers.container.console({ id: container.Id })"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Console</a>
<a class="btn" type="button" ui-sref="docker.containers.container.exec({ id: container.Id })"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Execute</a>
<a class="btn" type="button" ui-sref="docker.containers.container.attach({ id: container.Id })"><i class="fa fa-plug space-right" aria-hidden="true"></i>Attach</a>
</div>
</td>
</tr>

View File

@ -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();
}
};
};
}