mirror of https://github.com/portainer/portainer
feat(log-viewer): introduce the log viewer component (#1666)
parent
81de2a5afb
commit
0c5152fb5f
|
@ -13,6 +13,8 @@ angular.module('portainer', [
|
||||||
'angular-google-analytics',
|
'angular-google-analytics',
|
||||||
'angular-json-tree',
|
'angular-json-tree',
|
||||||
'angular-loading-bar',
|
'angular-loading-bar',
|
||||||
|
'angular-clipboard',
|
||||||
|
'luegg.directives',
|
||||||
'portainer.templates',
|
'portainer.templates',
|
||||||
'portainer.app',
|
'portainer.app',
|
||||||
'portainer.docker',
|
'portainer.docker',
|
||||||
|
|
|
@ -389,6 +389,17 @@ angular.module('portainer.docker', ['portainer.app'])
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var taskLogs = {
|
||||||
|
name: 'docker.tasks.task.logs',
|
||||||
|
url: '/logs',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/docker/views/tasks/logs/tasklogs.html',
|
||||||
|
controller: 'TaskLogsController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var templates = {
|
var templates = {
|
||||||
name: 'docker.templates',
|
name: 'docker.templates',
|
||||||
url: '/templates',
|
url: '/templates',
|
||||||
|
@ -488,6 +499,7 @@ angular.module('portainer.docker', ['portainer.app'])
|
||||||
$stateRegistryProvider.register(swarmVisualizer);
|
$stateRegistryProvider.register(swarmVisualizer);
|
||||||
$stateRegistryProvider.register(tasks);
|
$stateRegistryProvider.register(tasks);
|
||||||
$stateRegistryProvider.register(task);
|
$stateRegistryProvider.register(task);
|
||||||
|
$stateRegistryProvider.register(taskLogs);
|
||||||
$stateRegistryProvider.register(templates);
|
$stateRegistryProvider.register(templates);
|
||||||
$stateRegistryProvider.register(templatesLinuxServer);
|
$stateRegistryProvider.register(templatesLinuxServer);
|
||||||
$stateRegistryProvider.register(volumes);
|
$stateRegistryProvider.register(volumes);
|
||||||
|
|
|
@ -196,7 +196,7 @@
|
||||||
<td ng-if="$ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect">
|
<td ng-if="$ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect">
|
||||||
<div class="btn-group btn-group-xs" role="group" aria-label="..." style="display:inline-flex;">
|
<div class="btn-group btn-group-xs" role="group" aria-label="..." style="display:inline-flex;">
|
||||||
<a ng-if="$ctrl.settings.showQuickActionStats" style="margin: 0 2.5px;" ui-sref="docker.containers.container.stats({id: item.Id})" title="Stats"><i class="fa fa-area-chart space-right" aria-hidden="true"></i></a>
|
<a ng-if="$ctrl.settings.showQuickActionStats" style="margin: 0 2.5px;" ui-sref="docker.containers.container.stats({id: item.Id})" title="Stats"><i class="fa fa-area-chart space-right" aria-hidden="true"></i></a>
|
||||||
<a ng-if="$ctrl.settings.showQuickActionLogs" style="margin: 0 2.5px;" ui-sref="docker.containers.container.logs({id: item.Id})" title="Logs"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i></a>
|
<a ng-if="$ctrl.settings.showQuickActionLogs" style="margin: 0 2.5px;" ui-sref="docker.containers.container.logs({id: item.Id})" title="Logs"><i class="fa fa-file-text-o space-right" aria-hidden="true"></i></a>
|
||||||
<a ng-if="$ctrl.settings.showQuickActionConsole" style="margin: 0 2.5px;" ui-sref="docker.containers.container.console({id: item.Id})" title="Console"><i class="fa fa-terminal space-right" aria-hidden="true"></i></a>
|
<a ng-if="$ctrl.settings.showQuickActionConsole" style="margin: 0 2.5px;" ui-sref="docker.containers.container.console({id: item.Id})" title="Console"><i class="fa fa-terminal space-right" aria-hidden="true"></i></a>
|
||||||
<a ng-if="$ctrl.settings.showQuickActionInspect" style="margin: 0 2.5px;" ui-sref="docker.containers.container.inspect({id: item.Id})" title="Inspect"><i class="fa fa-info-circle space-right" aria-hidden="true"></i></a>
|
<a ng-if="$ctrl.settings.showQuickActionInspect" style="margin: 0 2.5px;" ui-sref="docker.containers.container.inspect({id: item.Id})" title="Inspect"><i class="fa fa-info-circle space-right" aria-hidden="true"></i></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Updated' && $ctrl.state.reverseOrder"></i>
|
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Updated' && $ctrl.state.reverseOrder"></i>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
|
<th ng-if="$ctrl.showLogsButton">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -63,6 +64,11 @@
|
||||||
<td ng-if="$ctrl.showSlotColumn">{{ item.Slot ? item.Slot : '-' }}</td>
|
<td ng-if="$ctrl.showSlotColumn">{{ item.Slot ? item.Slot : '-' }}</td>
|
||||||
<td>{{ item.NodeId | tasknodename: $ctrl.nodes }}</td>
|
<td>{{ item.NodeId | tasknodename: $ctrl.nodes }}</td>
|
||||||
<td>{{ item.Updated | getisodate }}</td>
|
<td>{{ item.Updated | getisodate }}</td>
|
||||||
|
<td ng-if="$ctrl.showLogsButton">
|
||||||
|
<a ui-sref="docker.tasks.task.logs({id: item.Id})">
|
||||||
|
<i class="fa fa-file-text-o" aria-hidden="true"></i> View logs
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!$ctrl.dataset">
|
<tr ng-if="!$ctrl.dataset">
|
||||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||||
|
|
|
@ -10,6 +10,7 @@ angular.module('portainer.docker').component('tasksDatatable', {
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
nodes: '<',
|
nodes: '<',
|
||||||
showTextFilter: '<',
|
showTextFilter: '<',
|
||||||
showSlotColumn: '<'
|
showSlotColumn: '<',
|
||||||
|
showLogsButton: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
angular.module('portainer.docker').component('logViewer', {
|
||||||
|
templateUrl: 'app/docker/components/log-viewer/logViewer.html',
|
||||||
|
controller: 'LogViewerController',
|
||||||
|
bindings: {
|
||||||
|
data: '=',
|
||||||
|
logCollectionChange: '<'
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,62 @@
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-file-text-o" title="Log viewer settings"></rd-widget-header>
|
||||||
|
<rd-widget-body>
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label for="tls" class="control-label text-left">
|
||||||
|
Log collection
|
||||||
|
<portainer-tooltip position="bottom" message="Disabling this option allows you to pause the log collection process."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<label class="switch" style="margin-left: 20px;">
|
||||||
|
<input type="checkbox" ng-model="$ctrl.state.logCollection" ng-change="$ctrl.logCollectionChange($ctrl.state.logCollection)"><i></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label for="tls" class="control-label text-left">
|
||||||
|
Auto-scrolling
|
||||||
|
</label>
|
||||||
|
<label class="switch" style="margin-left: 20px;">
|
||||||
|
<input type="checkbox" ng-model="$ctrl.state.autoScroll"><i></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="logs_search" class="col-sm-1 control-label text-left">
|
||||||
|
Search
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-11">
|
||||||
|
<input class="form-control" type="text" name="logs_search" ng-model="$ctrl.state.search" ng-change="$ctrl.state.selectedLines.length = 0;" placeholder="Filter...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-if="$ctrl.state.copySupported">
|
||||||
|
<label class="col-sm-1 control-label text-left">
|
||||||
|
Actions
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-11">
|
||||||
|
<button class="btn btn-primary btn-sm" ng-click="$ctrl.copy()" ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]) || !$ctrl.state.filteredLogs.length"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</button>
|
||||||
|
<button class="btn btn-primary btn-sm" ng-click="$ctrl.copySelection()" ng-disabled="($ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]) || !$ctrl.state.filteredLogs.length || !$ctrl.state.selectedLines.length"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy selected lines</button>
|
||||||
|
<span>
|
||||||
|
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" style="height:54%;">
|
||||||
|
<div class="col-sm-12" style="height:100%;">
|
||||||
|
<pre class="log_viewer" scroll-glue="$ctrl.state.autoScroll">
|
||||||
|
<div ng-repeat="line in $ctrl.state.filteredLogs = ($ctrl.data | filter:$ctrl.state.search) track by $index" class="line" ng-if="line"><p class="inner_line" ng-click="active=!active; $ctrl.selectLine(line)" ng-class="{'line_selected': active}">{{ line }}</p></div>
|
||||||
|
<div ng-if="!$ctrl.state.filteredLogs.length" class="line"><p class="inner_line">No log line matching the '{{ $ctrl.state.search }}' filter</p></div>
|
||||||
|
<div ng-if="$ctrl.state.filteredLogs.length === 1 && !$ctrl.state.filteredLogs[0]" class="line"><p class="inner_line">No logs available</p></div>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,35 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.controller('LogViewerController', ['clipboard',
|
||||||
|
function (clipboard) {
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
copySupported: clipboard.supported,
|
||||||
|
logCollection: true,
|
||||||
|
autoScroll: true,
|
||||||
|
search: '',
|
||||||
|
filteredLogs: [],
|
||||||
|
selectedLines: []
|
||||||
|
};
|
||||||
|
|
||||||
|
this.copy = function() {
|
||||||
|
clipboard.copyText(this.state.filteredLogs);
|
||||||
|
$('#refreshRateChange').show();
|
||||||
|
$('#refreshRateChange').fadeOut(1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.copySelection = function() {
|
||||||
|
clipboard.copyText(this.state.selectedLines);
|
||||||
|
$('#refreshRateChange').show();
|
||||||
|
$('#refreshRateChange').fadeOut(1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.selectLine = function(line) {
|
||||||
|
var idx = this.state.selectedLines.indexOf(line);
|
||||||
|
if (idx === -1) {
|
||||||
|
this.state.selectedLines.push(line);
|
||||||
|
} else {
|
||||||
|
this.state.selectedLines.splice(idx, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}]);
|
|
@ -36,3 +36,35 @@ function ContainerViewModel(data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ContainerStatsViewModel(data) {
|
||||||
|
this.Date = data.read;
|
||||||
|
this.MemoryUsage = data.memory_stats.usage;
|
||||||
|
this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage;
|
||||||
|
this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage;
|
||||||
|
this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage;
|
||||||
|
this.CurrentCPUSystemUsage = data.cpu_stats.system_cpu_usage;
|
||||||
|
if (data.cpu_stats.cpu_usage.percpu_usage) {
|
||||||
|
this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
|
||||||
|
}
|
||||||
|
this.Networks = _.values(data.networks);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContainerDetailsViewModel(data) {
|
||||||
|
this.Model = data;
|
||||||
|
this.Id = data.Id;
|
||||||
|
this.State = data.State;
|
||||||
|
this.Created = data.Created;
|
||||||
|
this.Name = data.Name;
|
||||||
|
this.NetworkSettings = data.NetworkSettings;
|
||||||
|
this.Args = data.Args;
|
||||||
|
this.Image = data.Image;
|
||||||
|
this.Config = data.Config;
|
||||||
|
this.HostConfig = data.HostConfig;
|
||||||
|
this.Mounts = data.Mounts;
|
||||||
|
if (data.Portainer) {
|
||||||
|
if (data.Portainer.ResourceControl) {
|
||||||
|
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
function ContainerDetailsViewModel(data) {
|
|
||||||
this.Model = data;
|
|
||||||
this.Id = data.Id;
|
|
||||||
this.State = data.State;
|
|
||||||
this.Created = data.Created;
|
|
||||||
this.Name = data.Name;
|
|
||||||
this.NetworkSettings = data.NetworkSettings;
|
|
||||||
this.Args = data.Args;
|
|
||||||
this.Image = data.Image;
|
|
||||||
this.Config = data.Config;
|
|
||||||
this.HostConfig = data.HostConfig;
|
|
||||||
this.Mounts = data.Mounts;
|
|
||||||
if (data.Portainer) {
|
|
||||||
if (data.Portainer.ResourceControl) {
|
|
||||||
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
function ContainerStatsViewModel(data) {
|
|
||||||
this.Date = data.read;
|
|
||||||
this.MemoryUsage = data.memory_stats.usage;
|
|
||||||
this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage;
|
|
||||||
this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage;
|
|
||||||
this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage;
|
|
||||||
this.CurrentCPUSystemUsage = data.cpu_stats.system_cpu_usage;
|
|
||||||
if (data.cpu_stats.cpu_usage.percpu_usage) {
|
|
||||||
this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
|
|
||||||
}
|
|
||||||
this.Networks = _.values(data.networks);
|
|
||||||
}
|
|
|
@ -13,6 +13,11 @@ angular.module('portainer.docker')
|
||||||
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
|
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
|
||||||
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
|
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
|
||||||
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
|
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
|
||||||
|
logs: {
|
||||||
|
method: 'GET', params: { id: '@id', action: 'logs' },
|
||||||
|
timeout: 4500, ignoreLoadingBar: true,
|
||||||
|
transformResponse: logsHandler, isArray: true
|
||||||
|
},
|
||||||
stats: {
|
stats: {
|
||||||
method: 'GET', params: { id: '@id', stream: false, action: 'stats' },
|
method: 'GET', params: { id: '@id', stream: false, action: 'stats' },
|
||||||
timeout: 4500, ignoreLoadingBar: true
|
timeout: 4500, ignoreLoadingBar: true
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
angular.module('portainer.docker')
|
|
||||||
.factory('ContainerLogs', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ContainerLogsFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
|
||||||
'use strict';
|
|
||||||
return {
|
|
||||||
get: function (id, params, callback) {
|
|
||||||
$http({
|
|
||||||
method: 'GET',
|
|
||||||
url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/containers/' + id + '/logs',
|
|
||||||
params: {
|
|
||||||
'stdout': params.stdout || 0,
|
|
||||||
'stderr': params.stderr || 0,
|
|
||||||
'timestamps': params.timestamps || 0,
|
|
||||||
'tail': params.tail || 'all'
|
|
||||||
},
|
|
||||||
ignoreLoadingBar: true
|
|
||||||
}).success(callback).error(function (data, status, headers, config) {
|
|
||||||
console.log(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}]);
|
|
|
@ -44,6 +44,18 @@ function genericHandler(data) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Docker API returns the logs as a single string.
|
||||||
|
// This handler will return an array with each line being an entry.
|
||||||
|
// It will also strip the 8 first characters of each line and remove any ANSI code related character sequences.
|
||||||
|
function logsHandler(data) {
|
||||||
|
var logs = data;
|
||||||
|
logs = logs.substring(8);
|
||||||
|
logs = logs.replace(/\n(.{8})/g, '\n\r');
|
||||||
|
logs = logs.replace(
|
||||||
|
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
||||||
|
return logs.split('\n');
|
||||||
|
}
|
||||||
|
|
||||||
// Image delete API returns an array on success (Docker 1.9 -> Docker 1.12).
|
// Image delete API returns an array on success (Docker 1.9 -> Docker 1.12).
|
||||||
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
|
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
|
||||||
// container the error (Docker = 1.12).
|
// container the error (Docker = 1.12).
|
||||||
|
|
|
@ -13,6 +13,11 @@ angular.module('portainer.docker')
|
||||||
ignoreLoadingBar: true
|
ignoreLoadingBar: true
|
||||||
},
|
},
|
||||||
update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} },
|
update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} },
|
||||||
remove: { method: 'DELETE', params: {id: '@id'} }
|
remove: { method: 'DELETE', params: {id: '@id'} },
|
||||||
|
logs: {
|
||||||
|
method: 'GET', params: { id: '@id', action: 'logs' },
|
||||||
|
timeout: 4500, ignoreLoadingBar: true,
|
||||||
|
transformResponse: logsHandler, isArray: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
angular.module('portainer.docker')
|
|
||||||
.factory('ServiceLogs', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ServiceLogsFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
|
||||||
'use strict';
|
|
||||||
return {
|
|
||||||
get: function (id, params, callback) {
|
|
||||||
$http({
|
|
||||||
method: 'GET',
|
|
||||||
url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/services/' + id + '/logs',
|
|
||||||
params: {
|
|
||||||
'stdout': params.stdout || 0,
|
|
||||||
'stderr': params.stderr || 0,
|
|
||||||
'timestamps': params.timestamps || 0,
|
|
||||||
'tail': params.tail || 'all'
|
|
||||||
}
|
|
||||||
}).success(callback).error(function (data, status, headers, config) {
|
|
||||||
console.log(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}]);
|
|
|
@ -1,11 +1,16 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('Task', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function TaskFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
.factory('Task', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function TaskFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||||
'use strict';
|
'use strict';
|
||||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/tasks/:id', {
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/tasks/:id/:action', {
|
||||||
endpointId: EndpointProvider.endpointID
|
endpointId: EndpointProvider.endpointID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
get: { method: 'GET', params: {id: '@id'} },
|
get: { method: 'GET', params: {id: '@id'} },
|
||||||
query: { method: 'GET', isArray: true, params: {filters: '@filters'} }
|
query: { method: 'GET', isArray: true, params: {filters: '@filters'} },
|
||||||
|
logs: {
|
||||||
|
method: 'GET', params: { id: '@id', action: 'logs' },
|
||||||
|
timeout: 4500, ignoreLoadingBar: true,
|
||||||
|
transformResponse: logsHandler, isArray: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -131,6 +131,18 @@ angular.module('portainer.docker')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.logs = function(id, stdout, stderr, timestamps, tail) {
|
||||||
|
var parameters = {
|
||||||
|
id: id,
|
||||||
|
stdout: stdout || 0,
|
||||||
|
stderr: stderr || 0,
|
||||||
|
timestamps: timestamps || 0,
|
||||||
|
tail: tail || 'all'
|
||||||
|
};
|
||||||
|
|
||||||
|
return Container.logs(parameters).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
service.containerStats = function(id) {
|
service.containerStats = function(id) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
|
|
@ -58,5 +58,17 @@ angular.module('portainer.docker')
|
||||||
return Service.update({ id: service.Id, version: service.Version }, config).$promise;
|
return Service.update({ id: service.Id, version: service.Version }, config).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.logs = function(id, stdout, stderr, timestamps, tail) {
|
||||||
|
var parameters = {
|
||||||
|
id: id,
|
||||||
|
stdout: stdout || 0,
|
||||||
|
stderr: stderr || 0,
|
||||||
|
timestamps: timestamps || 0,
|
||||||
|
tail: tail || 'all'
|
||||||
|
};
|
||||||
|
|
||||||
|
return Service.logs(parameters).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -35,5 +35,17 @@ angular.module('portainer.docker')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.logs = function(id, stdout, stderr, timestamps, tail) {
|
||||||
|
var parameters = {
|
||||||
|
id: id,
|
||||||
|
stdout: stdout || 0,
|
||||||
|
stderr: stderr || 0,
|
||||||
|
timestamps: timestamps || 0,
|
||||||
|
tail: tail || 'all'
|
||||||
|
};
|
||||||
|
|
||||||
|
return Task.logs(parameters).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -85,7 +85,7 @@
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<div class="btn-group" role="group" aria-label="...">
|
<div class="btn-group" role="group" aria-label="...">
|
||||||
<a class="btn" type="button" ui-sref="docker.containers.container.stats({id: container.Id})"><i class="fa fa-area-chart space-right" aria-hidden="true"></i>Stats</a>
|
<a class="btn" type="button" ui-sref="docker.containers.container.stats({id: container.Id})"><i class="fa fa-area-chart space-right" aria-hidden="true"></i>Stats</a>
|
||||||
<a class="btn" type="button" ui-sref="docker.containers.container.logs({id: container.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Logs</a>
|
<a class="btn" type="button" ui-sref="docker.containers.container.logs({id: container.Id})"><i class="fa fa-file-text-o space-right" aria-hidden="true"></i>Logs</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.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.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.inspect({id: container.Id})"><i class="fa fa-info-circle space-right" aria-hidden="true"></i>Inspect</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,70 +1,71 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ContainerLogsController', ['$scope', '$transition$', '$anchorScroll', 'ContainerLogs', 'Container', 'Notifications',
|
.controller('ContainerLogsController', ['$scope', '$transition$', '$interval', 'ContainerService', 'Notifications',
|
||||||
function ($scope, $transition$, $anchorScroll, ContainerLogs, Container, Notifications) {
|
function ($scope, $transition$, $interval, ContainerService, Notifications) {
|
||||||
$scope.state = {};
|
$scope.state = {
|
||||||
$scope.state.displayTimestampsOut = false;
|
refreshRate: 3,
|
||||||
$scope.state.displayTimestampsErr = false;
|
lineCount: 2000
|
||||||
$scope.stdout = '';
|
};
|
||||||
$scope.stderr = '';
|
|
||||||
$scope.tailLines = 2000;
|
|
||||||
|
|
||||||
Container.get({id: $transition$.params().id}, function (d) {
|
$scope.changeLogCollection = function(logCollectionStatus) {
|
||||||
$scope.container = d;
|
if (!logCollectionStatus) {
|
||||||
}, function (e) {
|
stopRepeater();
|
||||||
Notifications.error('Failure', e, 'Unable to retrieve container info');
|
} else {
|
||||||
});
|
setUpdateRepeater();
|
||||||
|
|
||||||
function getLogs() {
|
|
||||||
getLogsStdout();
|
|
||||||
getLogsStderr();
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
function getLogsStderr() {
|
|
||||||
ContainerLogs.get($transition$.params().id, {
|
|
||||||
stdout: 0,
|
|
||||||
stderr: 1,
|
|
||||||
timestamps: $scope.state.displayTimestampsErr,
|
|
||||||
tail: $scope.tailLines
|
|
||||||
}, function (data, status, headers, config) {
|
|
||||||
// Replace carriage returns with newlines to clean up output
|
|
||||||
data = data.replace(/[\r]/g, '\n');
|
|
||||||
// Strip 8 byte header from each line of output
|
|
||||||
data = data.substring(8);
|
|
||||||
data = data.replace(/\n(.{8})/g, '\n');
|
|
||||||
$scope.stderr = data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLogsStdout() {
|
|
||||||
ContainerLogs.get($transition$.params().id, {
|
|
||||||
stdout: 1,
|
|
||||||
stderr: 0,
|
|
||||||
timestamps: $scope.state.displayTimestampsOut,
|
|
||||||
tail: $scope.tailLines
|
|
||||||
}, function (data, status, headers, config) {
|
|
||||||
// Replace carriage returns with newlines to clean up output
|
|
||||||
data = data.replace(/[\r]/g, '\n');
|
|
||||||
// Strip 8 byte header from each line of output
|
|
||||||
data = data.substring(8);
|
|
||||||
data = data.replace(/\n(.{8})/g, '\n');
|
|
||||||
$scope.stdout = data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// initial call
|
|
||||||
getLogs();
|
|
||||||
var logIntervalId = window.setInterval(getLogs, 5000);
|
|
||||||
|
|
||||||
$scope.$on('$destroy', function() {
|
$scope.$on('$destroy', function() {
|
||||||
// clearing interval when view changes
|
stopRepeater();
|
||||||
clearInterval(logIntervalId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.toggleTimestampsOut = function () {
|
function stopRepeater() {
|
||||||
getLogsStdout();
|
var repeater = $scope.repeater;
|
||||||
};
|
if (angular.isDefined(repeater)) {
|
||||||
|
$interval.cancel(repeater);
|
||||||
|
repeater = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$scope.toggleTimestampsErr = function () {
|
function update(logs) {
|
||||||
getLogsStderr();
|
$scope.logs = logs;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function setUpdateRepeater() {
|
||||||
|
var refreshRate = $scope.state.refreshRate;
|
||||||
|
$scope.repeater = $interval(function() {
|
||||||
|
ContainerService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.logs = data;
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
stopRepeater();
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve container logs');
|
||||||
|
});
|
||||||
|
}, refreshRate * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startLogPolling() {
|
||||||
|
ContainerService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.logs = data;
|
||||||
|
setUpdateRepeater();
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
stopRepeater();
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve container logs');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
ContainerService.container($transition$.params().id)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.container = data;
|
||||||
|
startLogPolling();
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve container information');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -5,50 +5,6 @@
|
||||||
</rd-header-content>
|
</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row">
|
<log-viewer
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
data="logs" ng-if="logs" log-collection-change="changeLogCollection"
|
||||||
<rd-widget>
|
></log-viewer>
|
||||||
<rd-widget-body>
|
|
||||||
<div class="widget-icon grey pull-left">
|
|
||||||
<i class="fa fa-server"></i>
|
|
||||||
</div>
|
|
||||||
<div class="title">{{ container.Name|trimcontainername }}</div>
|
|
||||||
<div class="comment">Name</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="fa-info-circle" title="Stdout logs"></rd-widget-header>
|
|
||||||
<rd-widget-taskbar>
|
|
||||||
<input type="checkbox" ng-model="state.displayTimestampsOut" id="displayAllTsOut" ng-change="toggleTimestampsOut()"/>
|
|
||||||
<label for="displayAllTsOut">Display timestamps</label>
|
|
||||||
</rd-widget-taskbar>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="panel-body">
|
|
||||||
<pre id="stdoutLog" class="pre-scrollable pre-x-scrollable">{{stdout}}</pre>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="fa-exclamation-triangle" title="Stderr logs"></rd-widget-header>
|
|
||||||
<rd-widget-taskbar>
|
|
||||||
<input type="checkbox" ng-model="state.displayTimestampsErr" id="displayAllTsErr" ng-change="toggleTimestampsErr()"/>
|
|
||||||
<label for="displayAllTsErr">Display timestamps</label>
|
|
||||||
</rd-widget-taskbar>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="panel-body">
|
|
||||||
<pre id="stderrLog" class="pre-scrollable pre-x-scrollable">{{stderr}}</pre>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -6,5 +6,6 @@
|
||||||
nodes="nodes"
|
nodes="nodes"
|
||||||
show-text-filter="true"
|
show-text-filter="true"
|
||||||
show-slot-column="service.Mode !== 'global'"
|
show-slot-column="service.Mode !== 'global'"
|
||||||
|
show-logs-button="applicationState.endpoint.apiVersion >= 1.30"
|
||||||
></tasks-datatable>
|
></tasks-datatable>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<a ng-if="applicationState.endpoint.apiVersion >= 1.30" class="btn btn-primary btn-sm" type="button" ui-sref="docker.services.service.logs({id: service.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Service logs</a>
|
<a ng-if="applicationState.endpoint.apiVersion >= 1.30" class="btn btn-primary btn-sm" type="button" ui-sref="docker.services.service.logs({id: service.Id})"><i class="fa fa-file-text-o space-right" aria-hidden="true"></i>Service logs</a>
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.updateInProgress || isUpdating" ng-click="forceUpdateService(service)" button-spinner="state.updateInProgress" ng-if="applicationState.endpoint.apiVersion >= 1.25">
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.updateInProgress || isUpdating" ng-click="forceUpdateService(service)" button-spinner="state.updateInProgress" ng-if="applicationState.endpoint.apiVersion >= 1.25">
|
||||||
<span ng-hide="state.updateInProgress"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Update the service</span>
|
<span ng-hide="state.updateInProgress"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Update the service</span>
|
||||||
<span ng-show="state.updateInProgress">Update in progress...</span>
|
<span ng-show="state.updateInProgress">Update in progress...</span>
|
||||||
|
|
|
@ -1,78 +1,69 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ServiceLogsController', ['$scope', '$transition$', '$anchorScroll', 'ServiceLogs', 'Service',
|
.controller('ServiceLogsController', ['$scope', '$transition$', '$interval', 'ServiceService', 'Notifications',
|
||||||
function ($scope, $transition$, $anchorScroll, ServiceLogs, Service) {
|
function ($scope, $transition$, $interval, ServiceService, Notifications) {
|
||||||
$scope.state = {};
|
$scope.state = {
|
||||||
$scope.state.displayTimestampsOut = false;
|
refreshRate: 3,
|
||||||
$scope.state.displayTimestampsErr = false;
|
lineCount: 2000
|
||||||
$scope.stdout = '';
|
};
|
||||||
$scope.stderr = '';
|
|
||||||
$scope.tailLines = 2000;
|
|
||||||
|
|
||||||
function getLogs() {
|
$scope.changeLogCollection = function(logCollectionStatus) {
|
||||||
getLogsStdout();
|
if (!logCollectionStatus) {
|
||||||
getLogsStderr();
|
stopRepeater();
|
||||||
|
} else {
|
||||||
|
setUpdateRepeater();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function getLogsStderr() {
|
$scope.$on('$destroy', function() {
|
||||||
ServiceLogs.get($transition$.params().id, {
|
stopRepeater();
|
||||||
stdout: 0,
|
|
||||||
stderr: 1,
|
|
||||||
timestamps: $scope.state.displayTimestampsErr,
|
|
||||||
tail: $scope.tailLines
|
|
||||||
}, function (data, status, headers, config) {
|
|
||||||
// Replace carriage returns with newlines to clean up output
|
|
||||||
data = data.replace(/[\r]/g, '\n');
|
|
||||||
// Strip 8 byte header from each line of output
|
|
||||||
data = data.substring(8);
|
|
||||||
data = data.replace(/\n(.{8})/g, '\n');
|
|
||||||
$scope.stderr = data;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function stopRepeater() {
|
||||||
|
var repeater = $scope.repeater;
|
||||||
|
if (angular.isDefined(repeater)) {
|
||||||
|
$interval.cancel(repeater);
|
||||||
|
repeater = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLogsStdout() {
|
function setUpdateRepeater() {
|
||||||
ServiceLogs.get($transition$.params().id, {
|
var refreshRate = $scope.state.refreshRate;
|
||||||
stdout: 1,
|
$scope.repeater = $interval(function() {
|
||||||
stderr: 0,
|
ServiceService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
|
||||||
timestamps: $scope.state.displayTimestampsOut,
|
.then(function success(data) {
|
||||||
tail: $scope.tailLines
|
$scope.logs = data;
|
||||||
}, function (data, status, headers, config) {
|
})
|
||||||
// Replace carriage returns with newlines to clean up output
|
.catch(function error(err) {
|
||||||
data = data.replace(/[\r]/g, '\n');
|
stopRepeater();
|
||||||
// Strip 8 byte header from each line of output
|
Notifications.error('Failure', err, 'Unable to retrieve service logs');
|
||||||
data = data.substring(8);
|
|
||||||
data = data.replace(/\n(.{8})/g, '\n');
|
|
||||||
$scope.stdout = data;
|
|
||||||
});
|
});
|
||||||
|
}, refreshRate * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getService() {
|
function startLogPolling() {
|
||||||
Service.get({id: $transition$.params().id}, function (d) {
|
ServiceService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
|
||||||
$scope.service = d;
|
.then(function success(data) {
|
||||||
}, function (e) {
|
$scope.logs = data;
|
||||||
Notifications.error('Failure', e, 'Unable to retrieve service info');
|
console.log(JSON.stringify(data, null, 4));
|
||||||
|
setUpdateRepeater();
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
stopRepeater();
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve service logs');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
getService();
|
ServiceService.service($transition$.params().id)
|
||||||
getLogs();
|
.then(function success(data) {
|
||||||
|
$scope.service = data;
|
||||||
var logIntervalId = window.setInterval(getLogs, 5000);
|
startLogPolling();
|
||||||
|
})
|
||||||
$scope.$on('$destroy', function () {
|
.catch(function error(err) {
|
||||||
// clearing interval when view changes
|
Notifications.error('Failure', err, 'Unable to retrieve service information');
|
||||||
clearInterval(logIntervalId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.toggleTimestampsOut = function () {
|
|
||||||
getLogsStdout();
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.toggleTimestampsErr = function () {
|
|
||||||
getLogsStderr();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,54 +1,10 @@
|
||||||
<rd-header>
|
<rd-header>
|
||||||
<rd-header-title title="Service logs"></rd-header-title>
|
<rd-header-title title="Service logs"></rd-header-title>
|
||||||
<rd-header-content>
|
<rd-header-content>
|
||||||
<a ui-sref="docker.services">Services</a> > <a ui-sref="docker.services.service({id: service.ID})">{{ service.Spec.Name }}</a> > Logs
|
<a ui-sref="docker.services">Services</a> > <a ui-sref="docker.services.service({id: service.ID})">{{ service.Name }}</a> > Logs
|
||||||
</rd-header-content>
|
</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row">
|
<log-viewer
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
data="logs" ng-if="logs" log-collection-change="changeLogCollection"
|
||||||
<rd-widget>
|
></log-viewer>
|
||||||
<rd-widget-body>
|
|
||||||
<div class="widget-icon grey pull-left">
|
|
||||||
<i class="fa fa-list-alt"></i>
|
|
||||||
</div>
|
|
||||||
<div class="title">{{ service.Spec.Name }}</div>
|
|
||||||
<div class="comment">Name</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="fa-info-circle" title="Stdout logs"></rd-widget-header>
|
|
||||||
<rd-widget-taskbar>
|
|
||||||
<input type="checkbox" ng-model="state.displayTimestampsOut" id="displayAllTsOut" ng-change="toggleTimestampsOut()"/>
|
|
||||||
<label for="displayAllTsOut">Display timestamps</label>
|
|
||||||
</rd-widget-taskbar>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="panel-body">
|
|
||||||
<pre id="stdoutLog" class="pre-scrollable pre-x-scrollable">{{stdout}}</pre>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="fa-exclamation-triangle" title="Stderr logs"></rd-widget-header>
|
|
||||||
<rd-widget-taskbar>
|
|
||||||
<input type="checkbox" ng-model="state.displayTimestampsErr" id="displayAllTsErr" ng-change="toggleTimestampsErr()"/>
|
|
||||||
<label for="displayAllTsErr">Display timestamps</label>
|
|
||||||
</rd-widget-taskbar>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<div class="panel-body">
|
|
||||||
<pre id="stderrLog" class="pre-scrollable pre-x-scrollable">{{stderr}}</pre>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
nodes="nodes"
|
nodes="nodes"
|
||||||
show-text-filter="true"
|
show-text-filter="true"
|
||||||
show-slot-column="true"
|
show-slot-column="true"
|
||||||
|
show-logs-button="applicationState.endpoint.apiVersion >= 1.30"
|
||||||
></tasks-datatable>
|
></tasks-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,6 +44,9 @@
|
||||||
<td>Container ID</td>
|
<td>Container ID</td>
|
||||||
<td>{{ task.Status.ContainerStatus.ContainerID }}</td>
|
<td>{{ task.Status.ContainerStatus.ContainerID }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr ng-if="applicationState.endpoint.apiVersion >= 1.30" >
|
||||||
|
<td colspan="2"><a class="btn btn-primary btn-sm" type="button" ui-sref="docker.tasks.task.logs({id: task.Id})"><i class="fa fa-file-text-o space-right" aria-hidden="true"></i>Task logs</a></td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('TaskController', ['$scope', '$transition$', 'TaskService', 'Service', 'Notifications',
|
.controller('TaskController', ['$scope', '$transition$', 'TaskService', 'ServiceService', 'Notifications',
|
||||||
function ($scope, $transition$, TaskService, Service, Notifications) {
|
function ($scope, $transition$, TaskService, ServiceService, Notifications) {
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
TaskService.task($transition$.params().id)
|
TaskService.task($transition$.params().id)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var task = data;
|
var task = data;
|
||||||
$scope.task = task;
|
$scope.task = task;
|
||||||
return Service.get({ id: task.ServiceId }).$promise;
|
return ServiceService.service(task.ServiceId);
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var service = new ServiceViewModel(data);
|
var service = data;
|
||||||
$scope.service = service;
|
$scope.service = service;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.controller('TaskLogsController', ['$scope', '$transition$', '$interval', 'TaskService', 'ServiceService', 'Notifications',
|
||||||
|
function ($scope, $transition$, $interval, TaskService, ServiceService, Notifications) {
|
||||||
|
$scope.state = {
|
||||||
|
refreshRate: 3,
|
||||||
|
lineCount: 2000
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.changeLogCollection = function(logCollectionStatus) {
|
||||||
|
if (!logCollectionStatus) {
|
||||||
|
stopRepeater();
|
||||||
|
} else {
|
||||||
|
setUpdateRepeater();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$on('$destroy', function() {
|
||||||
|
stopRepeater();
|
||||||
|
});
|
||||||
|
|
||||||
|
function stopRepeater() {
|
||||||
|
var repeater = $scope.repeater;
|
||||||
|
if (angular.isDefined(repeater)) {
|
||||||
|
$interval.cancel(repeater);
|
||||||
|
repeater = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUpdateRepeater() {
|
||||||
|
var refreshRate = $scope.state.refreshRate;
|
||||||
|
$scope.repeater = $interval(function() {
|
||||||
|
TaskService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.logs = data;
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
stopRepeater();
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve task logs');
|
||||||
|
});
|
||||||
|
}, refreshRate * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startLogPolling() {
|
||||||
|
TaskService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.logs = data;
|
||||||
|
setUpdateRepeater();
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
stopRepeater();
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve task logs');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
TaskService.task($transition$.params().id)
|
||||||
|
.then(function success(data) {
|
||||||
|
var task = data;
|
||||||
|
$scope.task = task;
|
||||||
|
return ServiceService.service(task.ServiceId);
|
||||||
|
})
|
||||||
|
.then(function success(data) {
|
||||||
|
var service = data;
|
||||||
|
$scope.service = service;
|
||||||
|
startLogPolling();
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve task details');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
|
}]);
|
|
@ -0,0 +1,10 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Task details"></rd-header-title>
|
||||||
|
<rd-header-content ng-if="task && service">
|
||||||
|
<a ui-sref="docker.services">Services</a> > <a ui-sref="docker.services.service({id: service.Id })">{{ service.Name }}</a> > <a ui-sref="docker.tasks.task({id: task.Id })">{{ task.Id }}</a> > Logs
|
||||||
|
</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<log-viewer
|
||||||
|
data="logs" ng-if="logs" log-collection-change="changeLogCollection"
|
||||||
|
></log-viewer>
|
|
@ -6,7 +6,6 @@ function ($document, CodeMirrorService) {
|
||||||
this.$onInit = function() {
|
this.$onInit = function() {
|
||||||
$document.ready(function() {
|
$document.ready(function() {
|
||||||
var editorElement = $document[0].getElementById(ctrl.identifier);
|
var editorElement = $document[0].getElementById(ctrl.identifier);
|
||||||
if (editorElement) {
|
|
||||||
ctrl.editor = CodeMirrorService.applyCodeMirrorOnElement(editorElement, ctrl.yml, ctrl.readOnly);
|
ctrl.editor = CodeMirrorService.applyCodeMirrorOnElement(editorElement, ctrl.yml, ctrl.readOnly);
|
||||||
if (ctrl.onChange) {
|
if (ctrl.onChange) {
|
||||||
ctrl.editor.on('change', ctrl.onChange);
|
ctrl.editor.on('change', ctrl.onChange);
|
||||||
|
@ -14,7 +13,6 @@ function ($document, CodeMirrorService) {
|
||||||
if (ctrl.value) {
|
if (ctrl.value) {
|
||||||
ctrl.editor.setValue(ctrl.value);
|
ctrl.editor.setValue(ctrl.value);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uirouter/angularjs": "~1.0.6",
|
"@uirouter/angularjs": "~1.0.6",
|
||||||
"angular": "~1.5.0",
|
"angular": "~1.5.0",
|
||||||
|
"angular-clipboard": "^1.6.2",
|
||||||
"angular-cookies": "~1.5.0",
|
"angular-cookies": "~1.5.0",
|
||||||
"angular-google-analytics": "github:revolunet/angular-google-analytics#~1.1.9",
|
"angular-google-analytics": "github:revolunet/angular-google-analytics#~1.1.9",
|
||||||
"angular-json-tree": "1.0.1",
|
"angular-json-tree": "1.0.1",
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
"angular-sanitize": "~1.5.0",
|
"angular-sanitize": "~1.5.0",
|
||||||
"angular-ui-bootstrap": "~2.5.0",
|
"angular-ui-bootstrap": "~2.5.0",
|
||||||
"angular-utils-pagination": "~0.11.1",
|
"angular-utils-pagination": "~0.11.1",
|
||||||
|
"angularjs-scroll-glue": "^2.2.0",
|
||||||
"angularjs-slider": "^6.4.0",
|
"angularjs-slider": "^6.4.0",
|
||||||
"bootbox": "^4.4.0",
|
"bootbox": "^4.4.0",
|
||||||
"bootstrap": "~3.3.6",
|
"bootstrap": "~3.3.6",
|
||||||
|
|
|
@ -82,6 +82,8 @@ angular:
|
||||||
- node_modules/isteven-angular-multiselect/isteven-multi-select.js
|
- node_modules/isteven-angular-multiselect/isteven-multi-select.js
|
||||||
- node_modules/angular-json-tree/dist/angular-json-tree.js
|
- node_modules/angular-json-tree/dist/angular-json-tree.js
|
||||||
- node_modules/angular-loading-bar/build/loading-bar.js
|
- node_modules/angular-loading-bar/build/loading-bar.js
|
||||||
|
- node_modules/angularjs-scroll-glue/src/scrollglue.js
|
||||||
|
- node_modules/angular-clipboard/angular-clipboard.js
|
||||||
minified:
|
minified:
|
||||||
- node_modules/angular/angular.min.js
|
- node_modules/angular/angular.min.js
|
||||||
- node_modules/angular-ui-bootstrap/dist/ui-bootstrap-tpls.js
|
- node_modules/angular-ui-bootstrap/dist/ui-bootstrap-tpls.js
|
||||||
|
@ -100,3 +102,5 @@ angular:
|
||||||
- node_modules/isteven-angular-multiselect/isteven-multi-select.js
|
- node_modules/isteven-angular-multiselect/isteven-multi-select.js
|
||||||
- node_modules/angular-json-tree/dist/angular-json-tree.min.js
|
- node_modules/angular-json-tree/dist/angular-json-tree.min.js
|
||||||
- node_modules/angular-loading-bar/build/loading-bar.min.js
|
- node_modules/angular-loading-bar/build/loading-bar.min.js
|
||||||
|
- node_modules/angularjs-scroll-glue/src/scrollglue.js
|
||||||
|
- node_modules/angular-clipboard/angular-clipboard.js
|
||||||
|
|
|
@ -87,6 +87,10 @@ amdefine@>=0.0.4:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
|
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
|
||||||
|
|
||||||
|
angular-clipboard@^1.6.2:
|
||||||
|
version "1.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/angular-clipboard/-/angular-clipboard-1.6.2.tgz#4708e5a1dc94f3940ab89861ea1e19b26754154f"
|
||||||
|
|
||||||
angular-cookies@~1.5.0:
|
angular-cookies@~1.5.0:
|
||||||
version "1.5.11"
|
version "1.5.11"
|
||||||
resolved "https://registry.yarnpkg.com/angular-cookies/-/angular-cookies-1.5.11.tgz#88558de7c5044dcc3abeb79614d7ef8107ba49c0"
|
resolved "https://registry.yarnpkg.com/angular-cookies/-/angular-cookies-1.5.11.tgz#88558de7c5044dcc3abeb79614d7ef8107ba49c0"
|
||||||
|
@ -141,6 +145,10 @@ angular@1.x, angular@~1.5.0:
|
||||||
version "1.5.11"
|
version "1.5.11"
|
||||||
resolved "https://registry.yarnpkg.com/angular/-/angular-1.5.11.tgz#8c5ba7386f15965c9acf3429f6881553aada30d6"
|
resolved "https://registry.yarnpkg.com/angular/-/angular-1.5.11.tgz#8c5ba7386f15965c9acf3429f6881553aada30d6"
|
||||||
|
|
||||||
|
angularjs-scroll-glue@^2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/angularjs-scroll-glue/-/angularjs-scroll-glue-2.2.0.tgz#07d3399ac16ca874c63b6b5ee2ee30558b37e5d1"
|
||||||
|
|
||||||
angularjs-slider@^6.4.0:
|
angularjs-slider@^6.4.0:
|
||||||
version "6.4.3"
|
version "6.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/angularjs-slider/-/angularjs-slider-6.4.3.tgz#50a7d738ff28d6a20b85263bb022771887aabd85"
|
resolved "https://registry.yarnpkg.com/angularjs-slider/-/angularjs-slider-6.4.3.tgz#50a7d738ff28d6a20b85263bb022771887aabd85"
|
||||||
|
|
Loading…
Reference in New Issue