Merge branch 'release/1.13.5'

pull/1032/head 1.13.5
Anthony Lapenna 2017-07-13 18:08:46 +02:00
commit a438357b45
101 changed files with 1628 additions and 1270 deletions

View File

@ -11,11 +11,11 @@
[![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster.
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (Docker for Linux and Docker for Windows are supported).
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container).
**_Portainer_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm*.
**_Portainer_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm mode*.
## Demo
@ -34,8 +34,8 @@ Please note that the public demo cluster is **reset every 15min**.
* Issues: https://github.com/portainer/portainer/issues
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
* Slack (chat): https://portainer.io/slack/
* Gitter (chat): https://gitter.im/portainer/Lobby
* Slack: https://portainer.io/slack/
## Reporting bugs and contributing

View File

@ -117,7 +117,7 @@ func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []port
}
for idx, endpoint := range fileEndpoints {
if endpoint.Name == "" || endpoint.URL == "" {
if !isValidEndpoint(&endpoint) {
job.logger.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
continue
}

View File

@ -75,7 +75,7 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques
u, err := handler.UserService.UserByUsername(username)
if err == portainer.ErrUserNotFound {
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusBadRequest, handler.Logger)
return
} else if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)

View File

@ -85,6 +85,11 @@ func rewriteResponse(response *http.Response, newResponseData interface{}, statu
response.StatusCode = statusCode
response.Body = body
response.ContentLength = int64(len(jsonData))
if response.Header == nil {
response.Header = make(http.Header)
}
response.Header.Set("Content-Length", strconv.Itoa(len(jsonData)))
return nil
}

View File

@ -59,6 +59,8 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
return p.proxyServiceRequest(request)
} else if strings.HasPrefix(path, "/volumes") {
return p.proxyVolumeRequest(request)
} else if strings.HasPrefix(path, "/swarm") {
return p.proxySwarmRequest(request)
}
return p.executeDockerRequest(request)
@ -143,6 +145,10 @@ func (p *proxyTransport) proxyVolumeRequest(request *http.Request) (*http.Respon
}
}
func (p *proxyTransport) proxySwarmRequest(request *http.Request) (*http.Response, error) {
return p.administratorOperation(request)
}
// restrictedOperation ensures that the current user has the required authorizations
// before executing the original request.
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {

View File

@ -22,7 +22,7 @@ func AuthorizedResourceControlDeletion(resourceControl *portainer.ResourceContro
if teamAccessesCount > 0 {
for _, access := range resourceControl.TeamAccesses {
for _, membership := range context.UserMemberships {
if membership.TeamID == access.TeamID && membership.Role == portainer.TeamLeader {
if membership.TeamID == access.TeamID {
return true
}
}

View File

@ -305,7 +305,7 @@ type (
const (
// APIVersion is the version number of the Portainer API.
APIVersion = "1.13.4"
APIVersion = "1.13.5"
// DBVersion is the version number of the Portainer database.
DBVersion = 2
// DefaultTemplatesURL represents the default URL for the templates definitions.

View File

@ -20,11 +20,10 @@ angular.module('portainer', [
'portainer.services',
'auth',
'dashboard',
'common.accesscontrol.panel',
'common.accesscontrol.form',
'container',
'containerConsole',
'containerLogs',
'serviceLogs',
'containers',
'createContainer',
'createNetwork',
@ -166,7 +165,7 @@ angular.module('portainer', [
}
}
})
.state('logs', {
.state('containerlogs', {
url: '^/containers/:id/logs',
views: {
'content@': {
@ -179,6 +178,19 @@ angular.module('portainer', [
}
}
})
.state('servicelogs', {
url: '^/services/:id/logs',
views: {
'content@': {
templateUrl: 'app/components/serviceLogs/servicelogs.html',
controller: 'ServiceLogsController'
},
'sidebar@': {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('console', {
url: '^/containers/:id/console',
views: {

View File

@ -1,126 +0,0 @@
<div ng-controller="AccessControlFormController">
<div class="col-sm-12 form-section-title">
Access control
</div>
<!-- access-control-switch -->
<div class="form-group">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Enable access control
<portainer-tooltip position="bottom" message="When enabled, you can restrict the access and management of this resource."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input name="ownership" type="checkbox" ng-model="formValues.enableAccessControl" ng-click="synchronizeFormData()"><i></i>
</label>
</div>
</div>
<!-- !access-control-switch -->
<!-- restricted-access -->
<div class="form-group" ng-if="formValues.enableAccessControl" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div ng-if="isAdmin">
<input type="radio" id="access_administrators" ng-model="formValues.Ownership" ng-click="synchronizeFormData()" value="administrators">
<label for="access_administrators">
<div class="boxselector_header">
<i ng-class="'administrators' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Administrators
</div>
<p>I want to restrict the management of this resource to administrators only</p>
</label>
</div>
<div ng-if="isAdmin">
<input type="radio" id="access_restricted" ng-model="formValues.Ownership" ng-click="synchronizeFormData()" value="restricted">
<label for="access_restricted">
<div class="boxselector_header">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Restricted
</div>
<p>
I want to restrict the management of this resource to a set of users and/or teams
</p>
</label>
</div>
<div ng-if="!isAdmin">
<input type="radio" id="access_private" ng-model="formValues.Ownership" ng-click="synchronizeFormData()" value="private">
<label for="access_private">
<div class="boxselector_header">
<i ng-class="'private' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Private
</div>
<p>
I want to this resource to be manageable by myself only
</p>
</label>
</div>
<div ng-if="!isAdmin && availableTeams.length > 0">
<input type="radio" id="access_restricted" ng-model="formValues.Ownership" ng-click="synchronizeFormData()" value="restricted">
<label for="access_restricted">
<div class="boxselector_header">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Restricted
</div>
<p ng-if="availableTeams.length === 1">
I want any member of my team (<b>{{ availableTeams[0].Name }}</b>) to be able to manage this resource
</p>
<p ng-if="availableTeams.length > 1">
I want to restrict the management of this resource to one or more of my teams
</p>
</label>
</div>
</div>
</div>
<!-- restricted-access -->
<!-- authorized-teams -->
<div class="form-group" ng-if="formValues.enableAccessControl && formValues.Ownership === 'restricted' && (isAdmin || (!isAdmin && availableTeams.length > 1))" >
<div class="col-sm-12">
<label for="group-access" class="control-label text-left">
Authorized teams
<portainer-tooltip ng-if="isAdmin && availableTeams.length > 0" position="bottom" message="You can select which teams(s) will be able to manage this resource."></portainer-tooltip>
<portainer-tooltip ng-if="!isAdmin && availableTeams.length > 1" position="bottom" message="As you are a member of multiple teams, you can select which teams(s) will be able to manage this resource."></portainer-tooltip>
</label>
<span ng-if="isAdmin && availableTeams.length === 0" class="small text-muted" style="margin-left: 20px;">
You have not yet created any team. Head over the <a ui-sref="teams">teams view</a> to manage user teams.</span>
</span>
<span isteven-multi-select
ng-if="(isAdmin && availableTeams.length > 0) || (!isAdmin && availableTeams.length > 1)"
input-model="availableTeams"
output-model="formValues.Ownership_Teams"
button-label="Name"
item-label="Name"
tick-property="ticked"
helper-elements="filter"
search-property="Name"
on-item-click="synchronizeFormData()"
translation="{nothingSelected: 'Select one or more teams', search: 'Search...'}"
style="margin-left: 20px;"
</span>
</div>
</div>
<!-- !authorized-teams -->
<!-- authorized-users -->
<div class="form-group" ng-if="formValues.enableAccessControl && formValues.Ownership === 'restricted' && isAdmin">
<div class="col-sm-12">
<label for="group-access" class="control-label text-left">
Authorized users
<portainer-tooltip ng-if="isAdmin && availableUsers.length > 0" position="bottom" message="You can select which user(s) will be able to manage this resource."></portainer-tooltip>
</label>
<span ng-if="availableUsers.length === 0" class="small text-muted" style="margin-left: 20px;">
You have not yet created any user. Head over the <a ui-sref="users">users view</a> to manage users.</span>
</span>
<span isteven-multi-select
ng-if="availableUsers.length > 0"
input-model="availableUsers"
output-model="formValues.Ownership_Users"
button-label="Username"
item-label="Username"
tick-property="ticked"
helper-elements="filter"
search-property="Username"
on-item-click="synchronizeFormData()"
translation="{nothingSelected: 'Select one or more users', search: 'Search...'}"
style="margin-left: 20px;"
</span>
</div>
</div>
<!-- !authorized-users -->
</div>

View File

@ -1,55 +0,0 @@
angular.module('common.accesscontrol.form', [])
.controller('AccessControlFormController', ['$q', '$scope', '$state', 'UserService', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'ControllerDataPipeline',
function ($q, $scope, $state, UserService, ResourceControlService, Notifications, Authentication, ModalService, ControllerDataPipeline) {
$scope.availableTeams = [];
$scope.availableUsers = [];
$scope.formValues = {
enableAccessControl: true,
Ownership_Teams: [],
Ownership_Users: [],
Ownership: 'private'
};
$scope.synchronizeFormData = function() {
ControllerDataPipeline.setAccessControlFormData($scope.formValues.enableAccessControl,
$scope.formValues.Ownership, $scope.formValues.Ownership_Users, $scope.formValues.Ownership_Teams);
};
function initAccessControlForm() {
$('#loadingViewSpinner').show();
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true: false;
$scope.isAdmin = isAdmin;
if (isAdmin) {
$scope.formValues.Ownership = 'administrators';
}
$q.all({
availableTeams: UserService.userTeams(userDetails.ID),
availableUsers: isAdmin ? UserService.users(false) : []
})
.then(function success(data) {
$scope.availableUsers = data.availableUsers;
var availableTeams = data.availableTeams;
$scope.availableTeams = availableTeams;
if (!isAdmin && availableTeams.length === 1) {
$scope.formValues.Ownership_Teams = availableTeams;
}
$scope.synchronizeFormData();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve access control information');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
initAccessControlForm();
}]);

View File

@ -1,158 +0,0 @@
angular.module('common.accesscontrol.panel', [])
.controller('AccessControlPanelController', ['$q', '$scope', '$state', 'UserService', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'ControllerDataPipeline', 'FormValidator',
function ($q, $scope, $state, UserService, ResourceControlService, Notifications, Authentication, ModalService, ControllerDataPipeline, FormValidator) {
$scope.state = {
displayAccessControlPanel: false,
canEditOwnership: false,
editOwnership: false,
formValidationError: ''
};
$scope.formValues = {
Ownership: 'public',
Ownership_Users: [],
Ownership_Teams: []
};
$scope.authorizedUsers = [];
$scope.availableUsers = [];
$scope.authorizedTeams = [];
$scope.availableTeams = [];
$scope.confirmUpdateOwnership = function (force) {
if (!validateForm()) {
return;
}
ModalService.confirmAccessControlUpdate(function (confirmed) {
if(!confirmed) { return; }
updateOwnership();
});
};
function processOwnershipFormValues() {
var userIds = [];
angular.forEach($scope.formValues.Ownership_Users, function(user) {
userIds.push(user.Id);
});
var teamIds = [];
angular.forEach($scope.formValues.Ownership_Teams, function(team) {
teamIds.push(team.Id);
});
var administratorsOnly = $scope.formValues.Ownership === 'administrators' ? true : false;
return {
ownership: $scope.formValues.Ownership,
authorizedUserIds: administratorsOnly ? [] : userIds,
authorizedTeamIds: administratorsOnly ? [] : teamIds,
administratorsOnly: administratorsOnly
};
}
function validateForm() {
$scope.state.formValidationError = '';
var error = '';
var accessControlData = {
ownership: $scope.formValues.Ownership,
authorizedUsers: $scope.formValues.Ownership_Users,
authorizedTeams: $scope.formValues.Ownership_Teams
};
var isAdmin = $scope.isAdmin;
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
if (error) {
$scope.state.formValidationError = error;
return false;
}
return true;
}
function updateOwnership() {
$('#loadingViewSpinner').show();
var accessControlData = ControllerDataPipeline.getAccessControlData();
var resourceId = accessControlData.resourceId;
var ownershipParameters = processOwnershipFormValues();
ResourceControlService.applyResourceControlChange(accessControlData.resourceType, resourceId,
$scope.resourceControl, ownershipParameters)
.then(function success(data) {
Notifications.success('Access control successfully updated');
$state.reload();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update access control');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
function initAccessControlPanel() {
$('#loadingViewSpinner').show();
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true: false;
var userId = userDetails.ID;
$scope.isAdmin = isAdmin;
var accessControlData = ControllerDataPipeline.getAccessControlData();
var resourceControl = accessControlData.resourceControl;
$scope.resourceType = accessControlData.resourceType;
$scope.resourceControl = resourceControl;
if (isAdmin) {
if (resourceControl) {
$scope.formValues.Ownership = resourceControl.Ownership === 'private' ? 'restricted' : resourceControl.Ownership;
} else {
$scope.formValues.Ownership = 'public';
}
} else {
$scope.formValues.Ownership = 'public';
}
ResourceControlService.retrieveOwnershipDetails(resourceControl)
.then(function success(data) {
$scope.authorizedUsers = data.authorizedUsers;
$scope.authorizedTeams = data.authorizedTeams;
return ResourceControlService.retrieveUserPermissionsOnResource(userId, isAdmin, resourceControl);
})
.then(function success(data) {
$scope.state.canEditOwnership = data.isPartOfRestrictedUsers || data.isLeaderOfAnyRestrictedTeams;
$scope.state.canChangeOwnershipToTeam = data.isPartOfRestrictedUsers;
return $q.all({
availableUsers: isAdmin ? UserService.users(false) : [],
availableTeams: isAdmin || data.isPartOfRestrictedUsers ? UserService.userTeams(userId) : []
});
})
.then(function success(data) {
$scope.availableUsers = data.availableUsers;
angular.forEach($scope.availableUsers, function(user) {
var found = _.find($scope.authorizedUsers, { Id: user.Id });
if (found) {
user.selected = true;
}
});
$scope.availableTeams = data.availableTeams;
angular.forEach(data.availableTeams, function(team) {
var found = _.find($scope.authorizedTeams, { Id: team.Id });
if (found) {
team.selected = true;
}
});
if (data.availableTeams.length === 1) {
$scope.formValues.Ownership_Teams.push(data.availableTeams[0]);
}
$scope.state.displayAccessControlPanel = true;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve access control information');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
initAccessControlPanel();
}]);

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
<a ui-sref="containers">Containers</a> &gt; <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
</rd-header-content>
</rd-header>
@ -33,6 +33,10 @@
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>ID</td>
<td>{{ container.Id }}</td>
</tr>
<tr>
<td>Name</td>
<td ng-if="!container.edit">
@ -75,7 +79,7 @@
<td colspan="2">
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-outline-secondary" type="button" ui-sref="stats({id: container.Id})"><i class="fa fa-area-chart space-right" aria-hidden="true"></i>Stats</a>
<a class="btn btn-outline-secondary" type="button" ui-sref="logs({id: container.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Logs</a>
<a class="btn btn-outline-secondary" type="button" ui-sref="containerlogs({id: container.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Logs</a>
<a class="btn btn-outline-secondary" type="button" ui-sref="console({id: container.Id})"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Console</a>
</div>
</td>
@ -87,7 +91,13 @@
</div>
</div>
<div ng-include="'app/components/common/accessControlPanel/accessControlPanel.html'" ng-if="container && applicationState.application.authentication"></div>
<!-- access-control-panel -->
<por-access-control-panel
ng-if="container && applicationState.application.authentication"
resource-control="container.ResourceControl"
resource-type="'container'">
</por-access-control-panel>
<!-- !access-control-panel -->
<div ng-if="container.State.Health" class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
@ -114,7 +124,7 @@
</tbody>
</table>
</rd-widget-body>
</rd-widge>
</rd-widget>
</div>
</div>

View File

@ -1,6 +1,6 @@
angular.module('container', [])
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerService', 'ImageHelper', 'Network', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline',
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerService, ImageHelper, Network, Notifications, Pagination, ModalService, ControllerDataPipeline) {
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerService', 'ImageHelper', 'Network', 'Notifications', 'Pagination', 'ModalService',
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerService, ImageHelper, Network, Notifications, Pagination, ModalService) {
$scope.activityTime = 0;
$scope.portBindings = [];
$scope.config = {
@ -19,7 +19,6 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con
Container.get({id: $stateParams.id}, function (d) {
var container = new ContainerDetailsViewModel(d);
$scope.container = container;
ControllerDataPipeline.setAccessControlData('container', $stateParams.id, container.ResourceControl);
$scope.container.edit = false;
$scope.container.newContainerName = $filter('trimcontainername')(container.Name);
@ -86,7 +85,7 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con
$('#createImageSpinner').show();
var image = $scope.config.Image;
var registry = $scope.config.Registry;
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry);
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL);
ContainerCommit.commit({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
$('#createImageSpinner').hide();
update();

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content ng-if="state.loaded">
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
<a ui-sref="containers">Containers</a> &gt; <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> &gt; Console
</rd-header-content>
</rd-header>
@ -16,29 +16,53 @@
</div>
</rd-widget-header>
<rd-widget-body>
<form>
<div class="row">
<form class="form-horizontal">
<div ng-if="!state.connected">
<!-- command-list -->
<div class="col-sm-4">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-linux" aria-hidden="true" ng-if="imageOS == 'linux'"></i>
<i class="fa fa-windows" aria-hidden="true" ng-if="imageOS == 'windows'"></i>
</span>
<select class="form-control" ng-model="state.command" id="command">
<option value="bash" ng-if="imageOS == 'linux'">/bin/bash</option>
<option value="sh" ng-if="imageOS == 'linux'">/bin/sh</option>
<option value="powershell" ng-if="imageOS == 'windows'">powershell</option>
<option value="cmd.exe" ng-if="imageOS == 'windows'">cmd.exe</option>
</select>
<div class="form-group">
<label for="command" class="col-lg-1 text-left col-sm-2 control-label">Command</label>
<div class="col-lg-11 col-sm-10">
<div class="input-group" ng-if="!formValues.isCustomCommand">
<span class="input-group-addon">
<i class="fa fa-linux" aria-hidden="true" ng-if="imageOS == 'linux'"></i>
<i class="fa fa-windows" aria-hidden="true" ng-if="imageOS == 'windows'"></i>
</span>
<select class="form-control" ng-model="formValues.command" id="command">
<option value="bash" ng-if="imageOS == 'linux'">/bin/bash</option>
<option value="sh" ng-if="imageOS == 'linux'">/bin/sh</option>
<option value="powershell" ng-if="imageOS == 'windows'">powershell</option>
<option value="cmd.exe" ng-if="imageOS == 'windows'">cmd.exe</option>
</select>
</div>
<input class="form-control" ng-if="formValues.isCustomCommand" type="text" name="custom-command" ng-model="formValues.customCommand" placeholder="e.g. ps aux">
</div>
</div>
<!-- !command-list -->
<div class="form-group col-lg-12">
<label for="command" class="text-left control-label">Use custom command</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.isCustomCommand"><i></i>
</label>
</div>
<div class="form-group">
<label for="username" class="col-lg-1 text-left col-sm-2 control-label">
User
<portainer-tooltip position="bottom" message="Format is one of: user, user:group, uid or uid:gid"></portainer-tooltip>
</label>
<div class="col-lg-11 col-sm-10">
<input class="form-control" type="text" name="username" ng-model="formValues.user" placeholder="root">
</div>
</div>
<!-- !command-list -->
<div class="col-sm-8">
<button type="button" class="btn btn-primary" ng-click="connect()" ng-disabled="state.connected">Connect</button>
<button type="button" class="btn btn-default" ng-click="disconnect()" ng-disabled="!state.connected">Disconnect</button>
<div class="form-group">
<div class="col-lg-offset-1 col-sm-offset-2 col-lg-11 col-sm-10">
<button type="button" class="btn btn-primary" ng-click="connect()">Connect</button>
</div>
</div>
</div>
<div ng-if="state.connected">
<label>Exec into container as <code>{{ ::formValues.user || 'default user' }}</code> using command <code>{{ formValues.isCustomCommand ? formValues.customCommand : formValues.command }}</code></label>
<button type="button" class="btn btn-default" ng-click="disconnect()">Disconnect</button>
</div>
</form>
</rd-widget-body>
</rd-widget>

View File

@ -1,9 +1,10 @@
angular.module('containerConsole', [])
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Container', 'Image', 'Exec', '$timeout', 'EndpointProvider', 'Notifications',
function ($scope, $stateParams, Container, Image, Exec, $timeout, EndpointProvider, Notifications) {
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Container', 'Image', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ContainerService', 'ExecService',
function ($scope, $stateParams, Container, Image, EndpointProvider, Notifications, ContainerHelper, ContainerService, ExecService) {
$scope.state = {};
$scope.state.loaded = false;
$scope.state.connected = false;
$scope.formValues = {};
var socket, term;
@ -22,7 +23,7 @@ function ($scope, $stateParams, Container, Image, Exec, $timeout, EndpointProvid
} else {
Image.get({id: d.Image}, function(imgData) {
$scope.imageOS = imgData.Os;
$scope.state.command = imgData.Os === 'windows' ? 'powershell' : 'bash';
$scope.formValues.command = imgData.Os === 'windows' ? 'powershell' : 'bash';
$scope.state.loaded = true;
$('#loadingViewSpinner').hide();
}, function (e) {
@ -39,33 +40,36 @@ function ($scope, $stateParams, Container, Image, Exec, $timeout, EndpointProvid
$('#loadConsoleSpinner').show();
var termWidth = Math.round($('#terminal-container').width() / 8.2);
var termHeight = 30;
var command = $scope.formValues.isCustomCommand ?
$scope.formValues.customCommand : $scope.formValues.command;
var execConfig = {
id: $stateParams.id,
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
Cmd: $scope.state.command.replace(' ', ',').split(',')
User: $scope.formValues.user,
Cmd: ContainerHelper.commandStringToArray(command)
};
Container.exec(execConfig, function(d) {
if (d.message) {
$('#loadConsoleSpinner').hide();
Notifications.error('Error', {}, d.message);
var execId;
ContainerService.createExec(execConfig)
.then(function success(data) {
execId = data.Id;
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId + '&endpointId=' + EndpointProvider.endpointID();
if (url.indexOf('https') > -1) {
url = url.replace('https://', 'wss://');
} else {
var execId = d.Id;
resizeTTY(execId, termHeight, termWidth);
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId + '&endpointId=' + EndpointProvider.endpointID();
if (url.indexOf('https') > -1) {
url = url.replace('https://', 'wss://');
} else {
url = url.replace('http://', 'ws://');
}
initTerm(url, termHeight, termWidth);
url = url.replace('http://', 'ws://');
}
}, function (e) {
initTerm(url, termHeight, termWidth);
return ExecService.resizeTTY(execId, termHeight, termWidth, 2000);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to exec into container');
})
.finally(function final() {
$('#loadConsoleSpinner').hide();
Notifications.error('Failure', e, 'Unable to start an exec instance');
});
};
@ -79,19 +83,6 @@ function ($scope, $stateParams, Container, Image, Exec, $timeout, EndpointProvid
}
};
function resizeTTY(execId, height, width) {
$timeout(function() {
Exec.resize({id: execId, height: height, width: width}, function (d) {
if (d.message) {
Notifications.error('Error', {}, 'Unable to resize TTY');
}
}, function (e) {
Notifications.error('Failure', {}, 'Unable to resize TTY');
});
}, 2000);
}
function initTerm(url, height, width) {
socket = new WebSocket(url);
@ -103,7 +94,7 @@ function ($scope, $stateParams, Container, Image, Exec, $timeout, EndpointProvid
term.on('data', function (data) {
socket.send(data);
});
term.open(document.getElementById('terminal-container'));
term.open(document.getElementById('terminal-container'), true);
term.resize(width, height);
term.setOption('cursorBlink', true);

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
<a ui-sref="containers">Containers</a> &gt; <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> &gt; Logs
</rd-header-content>
</rd-header>

View File

@ -25,12 +25,12 @@
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">
<div class="btn-group" role="group" aria-label="...">
<button type="button" class="btn btn-success btn-responsive" ng-click="startAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
<button type="button" class="btn btn-danger btn-responsive" ng-click="stopAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
<button type="button" class="btn btn-success btn-responsive" ng-click="startAction()" ng-disabled="!state.selectedItemCount || state.noStoppedItemsSelected"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
<button type="button" class="btn btn-danger btn-responsive" ng-click="stopAction()" ng-disabled="!state.selectedItemCount || state.noRunningItemsSelected"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
<button type="button" class="btn btn-danger btn-responsive" ng-click="killAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="restartAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount || state.noRunningItemsSelected"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount || state.noPausedItemsSelected"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
<button type="button" class="btn btn-danger btn-responsive" ng-click="confirmRemoveAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
</div>
<a class="btn btn-primary" type="button" ui-sref="actions.create.container"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add container</a>
@ -137,5 +137,5 @@
</div>
</div>
</rd-widget-body>
<rd-widget>
</rd-widget>
</div>

View File

@ -41,6 +41,7 @@ angular.module('containers', [])
}
return model;
});
updateSelectionFlags();
$('#loadContainersSpinner').hide();
}, function (e) {
$('#loadContainersSpinner').hide();
@ -117,17 +118,15 @@ angular.module('containers', [])
angular.forEach($scope.state.filteredContainers, function (container) {
if (container.Checked !== allSelected) {
container.Checked = allSelected;
$scope.selectItem(container);
toggleItemSelection(container);
}
});
updateSelectionFlags();
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
toggleItemSelection(item);
updateSelectionFlags();
};
$scope.toggleGetAll = function () {
@ -187,6 +186,33 @@ angular.module('containers', [])
);
};
function toggleItemSelection(item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
}
function updateSelectionFlags() {
$scope.state.noStoppedItemsSelected = true;
$scope.state.noRunningItemsSelected = true;
$scope.state.noPausedItemsSelected = true;
$scope.containers.forEach(function(container) {
if(!container.Checked) {
return;
}
if(container.Status === 'paused') {
$scope.state.noPausedItemsSelected = false;
} else if(container.Status === 'stopped') {
$scope.state.noStoppedItemsSelected = false;
} else if(container.Status === 'running') {
$scope.state.noRunningItemsSelected = false;
}
} );
}
function retrieveSwarmHostsInfo(data) {
var swarm_hosts = {};
var systemStatus = data.SystemStatus;
@ -207,7 +233,7 @@ angular.module('containers', [])
$q.when(provider !== 'DOCKER_SWARM' || SystemService.info())
.then(function success(data) {
if (provider === 'DOCKER_SWARM') {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
$scope.swarm_hosts = retrieveSwarmHostsInfo(data);
}
update({all: $scope.state.displayAll ? 1 : 0});
})

View File

@ -1,8 +1,8 @@
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
// See app/components/templates/templatesController.js as a reference.
angular.module('createContainer', [])
.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'ControllerDataPipeline', 'FormValidator',
function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, ControllerDataPipeline, FormValidator) {
.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator',
function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator) {
$scope.formValues = {
alwaysPull: true,
@ -13,7 +13,8 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper,
Labels: [],
ExtraHosts: [],
IPv4: '',
IPv6: ''
IPv6: '',
AccessControlData: new AccessControlFormData()
};
$scope.state = {
@ -285,7 +286,7 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper,
$scope.create = function () {
$('#createContainerSpinner').show();
var accessControlData = ControllerDataPipeline.getAccessControlFormData();
var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true : false;

View File

@ -1,7 +1,7 @@
<rd-header>
<rd-header-title title="Create container"></rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > Add container
<a ui-sref="containers">Containers</a> &gt; Add container
</rd-header-content>
</rd-header>
@ -98,7 +98,7 @@
</div>
<!-- !port-mapping -->
<!-- access-control -->
<div ng-include="'app/components/common/accessControlForm/accessControlForm.html'" ng-if="applicationState.application.authentication"></div>
<por-access-control-form form-data="formValues.AccessControlData"></por-access-control-form>
<!-- !access-control -->
<!-- actions -->
<div class="col-sm-12 form-section-title">

View File

@ -1,6 +1,6 @@
angular.module('createNetwork', [])
.controller('CreateNetworkController', ['$scope', '$state', 'Notifications', 'Network',
function ($scope, $state, Notifications, Network) {
.controller('CreateNetworkController', ['$scope', '$state', 'Notifications', 'Network', 'LabelHelper',
function ($scope, $state, Notifications, Network, LabelHelper) {
$scope.formValues = {
DriverOptions: [],
Subnet: '',
@ -30,7 +30,7 @@ function ($scope, $state, Notifications, Network) {
};
$scope.addLabel = function() {
$scope.formValues.Labels.push({ name: '', value: ''});
$scope.formValues.Labels.push({ key: '', value: ''});
};
$scope.removeLabel = function(index) {
@ -74,13 +74,7 @@ function ($scope, $state, Notifications, Network) {
}
function prepareLabelsConfig(config) {
var labels = {};
$scope.formValues.Labels.forEach(function (label) {
if (label.name && label.value) {
labels[label.name] = label.value;
}
});
config.Labels = labels;
config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels);
}
function prepareConfiguration() {

View File

@ -1,7 +1,7 @@
<rd-header>
<rd-header-title title="Create network"></rd-header-title>
<rd-header-content>
<a ui-sref="networks">Networks</a> > Add network
<a ui-sref="networks">Networks</a> &gt; Add network
</rd-header-content>
</rd-header>
@ -90,7 +90,7 @@
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="display:none"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="registries">Registries</a> > Add registry
<a ui-sref="registries">Registries</a> &gt; Add registry
</rd-header-content>
</rd-header>

View File

@ -1,6 +1,6 @@
angular.module('createSecret', [])
.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService',
function ($scope, $state, Notifications, SecretService) {
.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper',
function ($scope, $state, Notifications, SecretService, LabelHelper) {
$scope.formValues = {
Name: '',
Data: '',
@ -9,7 +9,7 @@ function ($scope, $state, Notifications, SecretService) {
};
$scope.addLabel = function() {
$scope.formValues.Labels.push({ name: '', value: ''});
$scope.formValues.Labels.push({ key: '', value: ''});
};
$scope.removeLabel = function(index) {
@ -17,13 +17,7 @@ function ($scope, $state, Notifications, SecretService) {
};
function prepareLabelsConfig(config) {
var labels = {};
$scope.formValues.Labels.forEach(function (label) {
if (label.name && label.value) {
labels[label.name] = label.value;
}
});
config.Labels = labels;
config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels);
}
function prepareSecretData(config) {

View File

@ -1,7 +1,7 @@
<rd-header>
<rd-header-title title="Create secret"></rd-header-title>
<rd-header-content>
<a ui-sref="secrets">Secrets</a> > Add secret
<a ui-sref="secrets">Secrets</a> &gt; Add secret
</rd-header-content>
</rd-header>
@ -52,7 +52,7 @@
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>

View File

@ -1,8 +1,8 @@
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
// See app/components/templates/templatesController.js as a reference.
angular.module('createService', [])
.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'ControllerDataPipeline', 'FormValidator', 'RegistryService', 'HttpRequestHelper',
function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, Authentication, ResourceControlService, Notifications, ControllerDataPipeline, FormValidator, RegistryService, HttpRequestHelper) {
.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper',
function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper) {
$scope.formValues = {
Name: '',
@ -23,9 +23,11 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
Ports: [],
Parallelism: 1,
PlacementConstraints: [],
PlacementPreferences: [],
UpdateDelay: 0,
FailureAction: 'pause',
Secrets: []
Secrets: [],
AccessControlData: new AccessControlFormData()
};
$scope.state = {
@ -81,7 +83,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
};
$scope.addPlacementPreference = function() {
$scope.formValues.PlacementPreferences.push({ key: '', operator: '==', value: '' });
$scope.formValues.PlacementPreferences.push({ strategy: 'spread', value: '' });
};
$scope.removePlacementPreference = function(index) {
@ -89,7 +91,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
};
$scope.addLabel = function() {
$scope.formValues.Labels.push({ name: '', value: ''});
$scope.formValues.Labels.push({ key: '', value: ''});
};
$scope.removeLabel = function(index) {
@ -97,7 +99,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
};
$scope.addContainerLabel = function() {
$scope.formValues.ContainerLabels.push({ name: '', value: ''});
$scope.formValues.ContainerLabels.push({ key: '', value: ''});
};
$scope.removeContainerLabel = function(index) {
@ -170,21 +172,8 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
}
function prepareLabelsConfig(config, input) {
var labels = {};
input.Labels.forEach(function (label) {
if (label.name && label.value) {
labels[label.name] = label.value;
}
});
config.Labels = labels;
var containerLabels = {};
input.ContainerLabels.forEach(function (label) {
if (label.name && label.value) {
containerLabels[label.name] = label.value;
}
});
config.TaskTemplate.ContainerSpec.Labels = containerLabels;
config.Labels = LabelHelper.fromKeyValueToLabelHash(input.Labels);
config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(input.ContainerLabels);
}
function prepareVolumes(config, input) {
@ -213,8 +202,10 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
FailureAction: input.FailureAction
};
}
function preparePlacementConfig(config, input) {
config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(input.PlacementConstraints);
config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(input.PlacementPreferences);
}
function prepareSecretConfig(config, input) {
@ -296,7 +287,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
$scope.create = function createService() {
$('#createServiceSpinner').show();
var accessControlData = ControllerDataPipeline.getAccessControlFormData();
var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true : false;

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="services">Services</a> > Add service
<a ui-sref="services">Services</a> &gt; Add service
</rd-header-content>
</rd-header>
@ -101,7 +101,7 @@
</div>
<!-- !port-mapping -->
<!-- access-control -->
<div ng-include="'app/components/common/accessControlForm/accessControlForm.html'" ng-if="applicationState.application.authentication"></div>
<por-access-control-form form-data="formValues.AccessControlData"></por-access-control-form>
<!-- !access-control -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
@ -328,7 +328,7 @@
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
@ -355,7 +355,7 @@
<div ng-repeat="label in formValues.ContainerLabels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>

View File

@ -29,3 +29,29 @@
</div>
</div>
</form>
<form class="form-horizontal" style="margin-top: 15px;" ng-if="applicationState.endpoint.apiVersion >= 1.30">
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Placement preferences</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementPreference()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement preference
</span>
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="preference in formValues.PlacementPreferences" style="margin-top: 2px;">
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">strategy</span>
<input type="text" class="form-control" ng-model="preference.strategy" placeholder="e.g. spread">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="preference.value" placeholder="e.g. node.labels.datacenter">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementPreference($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</form>

View File

@ -1,10 +1,11 @@
angular.module('createVolume', [])
.controller('CreateVolumeController', ['$scope', '$state', 'VolumeService', 'SystemService', 'ResourceControlService', 'Authentication', 'Notifications', 'ControllerDataPipeline', 'FormValidator',
function ($scope, $state, VolumeService, SystemService, ResourceControlService, Authentication, Notifications, ControllerDataPipeline, FormValidator) {
.controller('CreateVolumeController', ['$scope', '$state', 'VolumeService', 'SystemService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator',
function ($scope, $state, VolumeService, SystemService, ResourceControlService, Authentication, Notifications, FormValidator) {
$scope.formValues = {
Driver: 'local',
DriverOptions: []
DriverOptions: [],
AccessControlData: new AccessControlFormData()
};
$scope.state = {
@ -40,8 +41,8 @@ function ($scope, $state, VolumeService, SystemService, ResourceControlService,
var driver = $scope.formValues.Driver;
var driverOptions = $scope.formValues.DriverOptions;
var volumeConfiguration = VolumeService.createVolumeConfiguration(name, driver, driverOptions);
var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails();
var accessControlData = ControllerDataPipeline.getAccessControlFormData();
var isAdmin = userDetails.role === 1 ? true : false;
if (!validateForm(accessControlData, isAdmin)) {
@ -69,16 +70,18 @@ function ($scope, $state, VolumeService, SystemService, ResourceControlService,
function initView() {
$('#loadingViewSpinner').show();
SystemService.getVolumePlugins()
.then(function success(data) {
$scope.availableVolumeDrivers = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve volume drivers');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
if ($scope.applicationState.endpoint.mode.provider !== 'DOCKER_SWARM') {
SystemService.getVolumePlugins()
.then(function success(data) {
$scope.availableVolumeDrivers = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve volume drivers');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
}
initView();

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="volumes">Volumes</a> > Add volume
<a ui-sref="volumes">Volumes</a> &gt; Add volume
</rd-header-content>
</rd-header>
@ -65,7 +65,7 @@
</div>
<!-- !driver-options -->
<!-- access-control -->
<div ng-include="'app/components/common/accessControlForm/accessControlForm.html'" ng-if="applicationState.application.authentication"></div>
<por-access-control-form form-data="formValues.AccessControlData"></por-access-control-form>
<!-- !access-control -->
<!-- actions -->
<div class="col-sm-12 form-section-title">

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="endpoints">Endpoints</a> > <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a>
<a ui-sref="endpoints">Endpoints</a> &gt; <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a>
</rd-header-content>
</rd-header>

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="endpoints">Endpoints</a> > <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a> > Access management
<a ui-sref="endpoints">Endpoints</a> &gt; <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a> &gt; Access management
</rd-header-content>
</rd-header>

View File

@ -14,8 +14,11 @@
<rd-widget-header icon="fa-exclamation-triangle" title="Endpoint management is not available">
</rd-widget-header>
<rd-widget-body>
<span class="small text-muted">Portainer has been started using the <code>--external-endpoints</code> flag. Endpoint management via the UI is disabled. You can still manage endpoint access.</span>
</rd-wigdet-body>
<span class="small text-muted">Portainer has been started using the <code>--external-endpoints</code> flag.
Endpoint management via the UI is disabled.
<span ng-if="applicationState.application.authentication">You can still manage endpoint access.</span>
</span>
</rd-widget-body>
</rd-widget>
</div>
</div>
@ -203,6 +206,6 @@
</div>
</div>
</rd-widget-body>
<rd-widget>
</rd-widget>
</div>
</div>

View File

@ -69,6 +69,6 @@
</div>
</div>
</rd-widget-body>
<rd-widget>
</rd-widget>
</div>
</div>

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="images">Images</a> > <a ui-sref="image({id: image.Id})">{{ image.Id }}</a>
<a ui-sref="images">Images</a> &gt; <a ui-sref="image({id: image.Id})">{{ image.Id }}</a>
</rd-header-content>
</rd-header>
@ -167,3 +167,58 @@
</rd-widget>
</div>
</div>
<div class="row" ng-if="history.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-clone" title="Image layers"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table id="image-layers" class="table">
<thead>
<th>
<a ng-click="order('Size')">
Size
<span ng-show="sortType == 'Size' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Size' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="order('CreatedBy')">
Layer
<span ng-show="sortType == 'CreatedBy' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'CreatedBy' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</thead>
<tbody>
<tr ng-repeat="layer in history | orderBy:sortType:sortReverse">
<td style="white-space:nowrap;">
{{ layer.Size | humansize }}
</td>
<td class="expand">
<div ng-if="layer.CreatedBy.length > 130">
<span id="layer-command-{{$index}}-full" style="display: none">
{{ layer.CreatedBy | imagelayercommand }}
</span>
<span id="layer-command-{{$index}}-short">
{{ layer.CreatedBy | imagelayercommand | truncate:130 }}
<span ng-if="layer.CreatedBy.length > 130" style="margin-left: 5px;">
<a id="layer-command-expander{{$index}}" class="btn" ng-click='toggleLayerCommand($index)'>
<i class="fa fa-plus-circle" aria-hidden="true"></i>
</a>
</span>
</span>
</div>
<div ng-if="layer.CreatedBy.length <= 130">
<span id="layer-command-{{$index}}-full">
{{ layer.CreatedBy | imagelayercommand }}
</span>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -6,6 +6,20 @@ function ($scope, $stateParams, $state, $timeout, ImageService, RegistryService,
Registry: ''
};
$scope.sortType = 'Size';
$scope.sortReverse = true;
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.toggleLayerCommand = function(layerId) {
$('#layer-command-expander'+layerId+' span').toggleClass('glyphicon-plus-sign glyphicon-minus-sign');
$('#layer-command-'+layerId+'-short').toggle();
$('#layer-command-'+layerId+'-full').toggle();
};
$scope.tagImage = function() {
$('#loadingViewSpinner').show();
var image = $scope.formValues.Image;
@ -108,6 +122,18 @@ function ($scope, $stateParams, $state, $timeout, ImageService, RegistryService,
.finally(function final() {
$('#loadingViewSpinner').hide();
});
$('#loadingViewSpinner').show();
ImageService.history($stateParams.id)
.then(function success(data) {
$scope.history = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve image history');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
retrieveImageDetails();

View File

@ -70,6 +70,17 @@
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
<span class="btn-group btn-group-sm pull-right" style="margin-right: 20px;" ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'">
<label class="btn btn-primary" ng-model="state.containersCountFilter" uib-btn-radio="undefined">
All
</label>
<label class="btn btn-primary" ng-model="state.containersCountFilter" uib-btn-radio="'!' + 0">
Used
</label>
<label class="btn btn-primary" ng-model="state.containersCountFilter" uib-btn-radio="0">
Unused
</label>
</span>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
@ -110,9 +121,11 @@
</tr>
</thead>
<tbody>
<tr dir-paginate="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<tr dir-paginate="image in (state.filteredImages = (images | filter:{ Containers: state.containersCountFilter } | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
<td><a ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a></td>
<td>
<a class="monospaced" ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a>
<span style="margin-left: 10px;" class="label label-warning image-tag" ng-if="::image.Containers === 0 && applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'">Unused</span></td>
<td>
<span class="label label-primary image-tag" ng-repeat="tag in (image|repotags)">{{ tag }}</span>
</td>
@ -132,6 +145,6 @@
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>
</rd-widget>
</div>
</div>

View File

@ -93,7 +93,8 @@ function ($scope, $state, ImageService, Notifications, Pagination, ModalService)
function fetchImages() {
$('#loadImagesSpinner').show();
ImageService.images()
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
ImageService.images(endpointProvider !== 'DOCKER_SWARM')
.then(function success(data) {
$scope.images = data;
})

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="networks">Networks</a> > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
<a ui-sref="networks">Networks</a> &gt; <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
</rd-header-content>
</rd-header>

View File

@ -134,7 +134,7 @@
<tr dir-paginate="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td>
<td><a ui-sref="network({id: network.Id})">{{ network.Name|truncate:40}}</a></td>
<td>{{ network.Id }}</td>
<td class="monospaced">{{ network.Id|truncate:20 }}</td>
<td>{{ network.Scope }}</td>
<td>{{ network.Driver }}</td>
<td>{{ network.IPAM.Driver }}</td>
@ -154,6 +154,6 @@
</div>
</div>
</rd-widget-body>
<rd-widget>
</rd-widget>
</div>
</div>

View File

@ -5,7 +5,7 @@
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="swarm">Swarm nodes</a> > <a ui-sref="node({id: node.Id})">{{ node.Hostname }}</a>
<a ui-sref="swarm">Swarm nodes</a> &gt; <a ui-sref="node({id: node.Id})">{{ node.Hostname }}</a>
</rd-header-content>
</rd-header>

View File

@ -133,10 +133,10 @@
</td>
</tr>
<tr ng-if="!registries">
<td colspan="3" class="text-center text-muted">Loading...</td>
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="registries.length == 0">
<td colspan="3" class="text-center text-muted">No registries available.</td>
<td colspan="4" class="text-center text-muted">No registries available.</td>
</tr>
</tbody>
</table>
@ -145,6 +145,6 @@
</div>
</div>
</rd-widget-body>
<rd-widget>
</rd-widget>
</div>
</div>

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="registries">Registries</a> > <a ui-sref="registry({id: registry.Id})">{{ registry.Name }}</a>
<a ui-sref="registries">Registries</a> &gt; <a ui-sref="registry({id: registry.Id})">{{ registry.Name }}</a>
</rd-header-content>
</rd-header>

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="registries">Registries</a> > <a ui-sref="registry({id: registry.Id})">{{ registry.Name }}</a> > Access management
<a ui-sref="registries">Registries</a> &gt; <a ui-sref="registry({id: registry.Id})">{{ registry.Name }}</a> &gt; Access management
</rd-header-content>
</rd-header>

View File

@ -6,7 +6,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="secrets">Secrets</a> > <a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a>
<a ui-sref="secrets">Secrets</a> &gt; <a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a>
</rd-header-content>
</rd-header>

View File

@ -63,6 +63,6 @@
</div>
</div>
</rd-widget-body>
<rd-widget>
</rd-widget>
</div>
</div>

View File

@ -0,0 +1,57 @@
<div ng-if="service.ServicePreferences && applicationState.endpoint.apiVersion >= 1.30" id="service-placement-preferences">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Placement preferences">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating || addPlacementPreference(service)" ng-disabled="isUpdating">
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement preference
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="service.ServicePreferences.length === 0">
<p>There are no placement preferences for this service.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.ServicePreferences.length > 0" classes="no-padding">
<table class="table" >
<thead>
<tr>
<th>Strategy</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="preference in service.ServicePreferences">
<td>
<div class="input-group input-group-sm">
<input type="text" class="form-control" ng-model="preference.strategy" placeholder="e.g. node.role" ng-change="updatePlacementPreference(service, preference)" ng-disabled="isUpdating">
</div>
</td>
<td>
<div class="input-group input-group-sm">
<input type="text" class="form-control" ng-model="preference.value" placeholder="e.g. manager" ng-change="updatePlacementPreference(service, preference)" ng-disabled="isUpdating">
<span class="input-group-btn">
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementPreference(service, $index)" ng-disabled="isUpdating">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServicePreferences'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(service, ['ServicePreferences'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>

View File

@ -63,7 +63,7 @@
</table>
</rd-widget-body>
<rd-widget-footer>
<div class="btn-toolbar" role="toolbar"
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['Ports'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">

View File

@ -49,7 +49,7 @@
</thead>
<tbody>
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
<td><a ui-sref="task({ id: task.Id })" class="monospaced">{{ task.Id }}</a></td>
<td><span class="label label-{{ task.Status.State|taskstatusbadge }}">{{ task.Status.State }}</span></td>
<td ng-if="service.Mode !== 'global'">{{ task.Slot }}</td>
<td>{{ task.NodeId | tasknodename: nodes }}</td>

View File

@ -6,7 +6,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="services">Services</a> > <a ui-sref="service({id: service.Id})">{{ service.Name }}</a>
<a ui-sref="services">Services</a> &gt; <a ui-sref="service({id: service.Id})">{{ service.Name }}</a>
</rd-header-content>
</rd-header>
@ -72,6 +72,13 @@
<input type="text" class="form-control" ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" ng-disabled="isUpdating" />
</td>
</tr>
<tr ng-if="applicationState.endpoint.apiVersion >= 1.30">
<td colspan="2">
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-outline-secondary" type="button" ui-sref="servicelogs({id: service.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Logs</a>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
@ -106,6 +113,7 @@
<li><a href ng-click="goToItem('service-network-specs')">Network &amp; published ports</a></li>
<li><a href ng-click="goToItem('service-resources')">Resource limits &amp; reservations</a></li>
<li><a href ng-click="goToItem('service-placement-constraints')">Placement constraints</a></li>
<li><a href ng-click="goToItem('service-placement-preferences')" ng-if="applicationState.endpoint.apiVersion >= 1.30">Placement preferences</a></li>
<li><a href ng-click="goToItem('service-restart-policy')">Restart policy</a></li>
<li><a href ng-click="goToItem('service-update-config')">Update configuration</a></li>
<li><a href ng-click="goToItem('service-labels')">Service labels</a></li>
@ -117,7 +125,13 @@
</div>
</div>
<div ng-include="'app/components/common/accessControlPanel/accessControlPanel.html'" ng-if="service && applicationState.application.authentication"></div>
<!-- access-control-panel -->
<por-access-control-panel
ng-if="service && applicationState.application.authentication"
resource-control="service.ResourceControl"
resource-type="'service'">
</por-access-control-panel>
<!-- !access-control-panel -->
<div class="row">
<hr>
@ -145,6 +159,7 @@
<h3 id="service-specs">Service specification</h3>
<div id="service-resources" class="padding-top" ng-include="'app/components/service/includes/resources.html'"></div>
<div id="service-placement-constraints" class="padding-top" ng-include="'app/components/service/includes/constraints.html'"></div>
<div id="service-placement-preferences" class="padding-top" ng-include="'app/components/service/includes/placementPreferences.html'"></div>
<div id="service-restart-policy" class="padding-top" ng-include="'app/components/service/includes/restart.html'"></div>
<div id="service-update-config" class="padding-top" ng-include="'app/components/service/includes/updateconfig.html'"></div>
<div id="service-labels" class="padding-top" ng-include="'app/components/service/includes/servicelabels.html'"></div>

View File

@ -1,6 +1,6 @@
angular.module('service', [])
.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'Secret', 'SecretHelper', 'Service', 'ServiceHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline',
function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, ServiceService, Secret, SecretHelper, Service, ServiceHelper, TaskService, NodeService, Notifications, Pagination, ModalService, ControllerDataPipeline) {
.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'Secret', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService',
function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, ServiceService, Secret, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, Pagination, ModalService) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
@ -124,10 +124,24 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
}
};
$scope.updatePlacementConstraint = function updatePlacementConstraint(service, constraint) {
$scope.updatePlacementConstraint = function(service, constraint) {
updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
};
$scope.addPlacementPreference = function(service) {
service.ServicePreferences.push({ strategy: 'spread', value: '' });
updateServiceArray(service, 'ServicePreferences', service.ServicePreferences);
};
$scope.removePlacementPreference = function(service, index) {
var removedElement = service.ServicePreferences.splice(index, 1);
if (removedElement !== null) {
updateServiceArray(service, 'ServicePreferences', service.ServicePreferences);
}
};
$scope.updatePlacementPreference = function(service, constraint) {
updateServiceArray(service, 'ServicePreferences', service.ServicePreferences);
};
$scope.addPublishedPort = function addPublishedPort(service) {
if (!service.Ports) {
service.Ports = [];
@ -174,9 +188,9 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
$('#loadingViewSpinner').show();
var config = ServiceHelper.serviceToConfig(service.Model);
config.Name = service.Name;
config.Labels = translateServiceLabelsToLabels(service.ServiceLabels);
config.TaskTemplate.ContainerSpec.Env = translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
config.TaskTemplate.ContainerSpec.Labels = translateServiceLabelsToLabels(service.ServiceContainerLabels);
config.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceLabels);
config.TaskTemplate.ContainerSpec.Env = ServiceHelper.translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceContainerLabels);
config.TaskTemplate.ContainerSpec.Image = service.Image;
config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : [];
@ -188,6 +202,7 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
config.TaskTemplate.Placement = {};
}
config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(service.ServiceConstraints);
config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(service.ServicePreferences);
config.TaskTemplate.Resources = {
Limits: {
@ -263,11 +278,12 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
function translateServiceArrays(service) {
service.ServiceSecrets = service.Secrets ? service.Secrets.map(SecretHelper.flattenSecret) : [];
service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
service.EnvironmentVariables = ServiceHelper.translateEnvironmentVariables(service.Env);
service.ServiceLabels = LabelHelper.fromLabelHashToKeyValue(service.Labels);
service.ServiceContainerLabels = LabelHelper.fromLabelHashToKeyValue(service.ContainerLabels);
service.ServiceMounts = angular.copy(service.Mounts);
service.ServiceConstraints = translateConstraintsToKeyValue(service.Constraints);
service.ServiceConstraints = ServiceHelper.translateConstraintsToKeyValue(service.Constraints);
service.ServicePreferences = ServiceHelper.translatePreferencesToKeyValue(service.Preferences);
}
function initView() {
@ -283,7 +299,6 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
translateServiceArrays(service);
$scope.service = service;
ControllerDataPipeline.setAccessControlData('service', $stateParams.id, service.ResourceControl);
originalService = angular.copy(service);
return $q.all({
@ -310,7 +325,6 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
Notifications.error('Failure', err, 'Unable to retrieve service details');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
@ -341,80 +355,5 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
service.hasChanges = true;
}
function translateEnvironmentVariables(env) {
if (env) {
var variables = [];
env.forEach(function(variable) {
var idx = variable.indexOf('=');
var keyValue = [variable.slice(0,idx), variable.slice(idx+1)];
var originalValue = (keyValue.length > 1) ? keyValue[1] : '';
variables.push({ key: keyValue[0], value: originalValue, originalKey: keyValue[0], originalValue: originalValue, added: true});
});
return variables;
}
return [];
}
function translateEnvironmentVariablesToEnv(env) {
if (env) {
var variables = [];
env.forEach(function(variable) {
if (variable.key && variable.key !== '') {
variables.push(variable.key + '=' + variable.value);
}
});
return variables;
}
return [];
}
function translateLabelsToServiceLabels(Labels) {
var labels = [];
if (Labels) {
Object.keys(Labels).forEach(function(key) {
labels.push({ key: key, value: Labels[key], originalKey: key, originalValue: Labels[key], added: true});
});
}
return labels;
}
function translateServiceLabelsToLabels(labels) {
var Labels = {};
if (labels) {
labels.forEach(function(label) {
Labels[label.key] = label.value;
});
}
return Labels;
}
function translateConstraintsToKeyValue(constraints) {
function getOperator(constraint) {
var indexEquals = constraint.indexOf('==');
if (indexEquals >= 0) {
return [indexEquals, '=='];
}
return [constraint.indexOf('!='), '!='];
}
if (constraints) {
var keyValueConstraints = [];
constraints.forEach(function(constraint) {
var operatorIndices = getOperator(constraint);
var key = constraint.slice(0, operatorIndices[0]);
var operator = operatorIndices[1];
var value = constraint.slice(operatorIndices[0] + 2);
keyValueConstraints.push({
key: key,
value: value,
operator: operator,
originalKey: key,
originalValue: value
});
});
return keyValueConstraints;
}
return [];
}
initView();
}]);

View File

@ -0,0 +1,83 @@
angular.module('serviceLogs', [])
.controller('ServiceLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ServiceLogs', 'Service',
function ($scope, $stateParams, $anchorScroll, ServiceLogs, Service) {
$scope.state = {};
$scope.state.displayTimestampsOut = false;
$scope.state.displayTimestampsErr = false;
$scope.stdout = '';
$scope.stderr = '';
$scope.tailLines = 2000;
function getLogs() {
$('#loadingViewSpinner').show();
getLogsStdout();
getLogsStderr();
$('#loadingViewSpinner').hide();
}
function getLogsStderr() {
ServiceLogs.get($stateParams.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() {
ServiceLogs.get($stateParams.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;
});
}
function getService() {
$('#loadingViewSpinner').show();
Service.get({id: $stateParams.id}, function (d) {
$scope.service = d;
$('#loadingViewSpinner').hide();
}, function (e) {
Notifications.error('Failure', e, 'Unable to retrieve service info');
$('#loadingViewSpinner').hide();
});
}
function initView() {
getService();
getLogs();
var logIntervalId = window.setInterval(getLogs, 5000);
$scope.$on('$destroy', function () {
// clearing interval when view changes
clearInterval(logIntervalId);
});
$scope.toggleTimestampsOut = function () {
getLogsStdout();
};
$scope.toggleTimestampsErr = function () {
getLogsStderr();
};
}
initView();
}]);

View File

@ -0,0 +1,56 @@
<rd-header>
<rd-header-title title="Service logs">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="services">Services</a> > <a ui-sref="service({id: service.ID})">{{ service.Spec.Name }}</a> > Logs
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<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>

View File

@ -128,6 +128,6 @@
</div>
</div>
</rd-widget-body>
<rd-widget>
</rd-widget>
</div>
</div>

View File

@ -58,7 +58,7 @@
<li class="sidebar-list" ng-if="applicationState.application.authentication && (isAdmin || isTeamLeader)">
<a ui-sref="users" ui-sref-active="active">User management <span class="menu-icon fa fa-users"></span></a>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'users' || $state.current.name === 'user' || $state.current.name === 'teams' || $state.current.name === 'team')">
<a ui-sref="teams" ui-sref-active="active">Teams</span></a>
<a ui-sref="teams" ui-sref-active="active">Teams</a>
</div>
</li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">

View File

@ -49,7 +49,7 @@ function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointP
EndpointService.endpoints()
.then(function success(data) {
var endpoints = data;
$scope.endpoints = endpoints;
$scope.endpoints = _.sortBy(endpoints, ['Name']);
setActiveEndpoint(endpoints);
if (StateManager.getState().application.authentication) {

View File

@ -1,7 +1,7 @@
<rd-header>
<rd-header-title title="Container stats"></rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
<a ui-sref="containers">Containers</a> &gt; <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> &gt; Stats
</rd-header-content>
</rd-header>

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content ng-if="task && service">
<a ui-sref="services">Services</a> > <a ui-sref="service({id: service.Id })">{{ service.Name }}</a> > {{ task.Id }}
<a ui-sref="services">Services</a> &gt; <a ui-sref="service({id: service.Id })">{{ service.Name }}</a> &gt; {{ task.Id }}
</rd-header-content>
</rd-header>

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="teams">Teams</a> > <a ui-sref="team({id: team.Id})">{{ team.Name }}</a>
<a ui-sref="teams">Teams</a> &gt; <a ui-sref="team({id: team.Id})">{{ team.Name }}</a>
</rd-header-content>
</rd-header>
@ -144,7 +144,7 @@
<tr pagination-id="table2" dir-paginate="user in teamMembers | filter:state.filterGroupMembers | orderBy:sortTypeGroupMembers:sortReverseGroupMembers | itemsPerPage: state.pagination_count_groupMembers">
<td>
{{ user.Username }}
<span style="margin-left: 5px;" ng-if="isAdmin || user.TeamRole === 'Member'")>
<span style="margin-left: 5px;" ng-if="isAdmin || user.TeamRole === 'Member'">
<a class="btn-outline-secondary" ng-click="removeUser(user)"><i class="fa fa-minus-circle space-right" aria-hidden="true"></i>Remove</a>
</span>
</td>

View File

@ -43,8 +43,8 @@
helper-elements="filter"
search-property="Username"
translation="{nothingSelected: 'Select one or more team leaders', search: 'Search...'}"
style="margin-left: 20px;"
</span>
style="margin-left: 20px;">
</span>
</div>
</div>
<!-- !team-leaders -->
@ -125,6 +125,6 @@
</div>
</div>
</rd-widget-body>
<rd-widget>
</rd-widget>
</div>
</div>

View File

@ -68,7 +68,7 @@
</div>
<!-- !env -->
<!-- access-control -->
<div ng-include="'app/components/common/accessControlForm/accessControlForm.html'" ng-if="applicationState.application.authentication"></div>
<por-access-control-form form-data="formValues.AccessControlData"></por-access-control-form>
<!-- !access-control -->
<div class="form-group">
<div class="col-sm-12">
@ -202,13 +202,14 @@
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
<i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<span class="small text-muted" style="margin-left: 10px" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<span class="small text-muted" style="margin-left: 10px" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && !state.formValidationError">
When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.
</span>
<span ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'" style="margin-left: 10px">
<span ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && !state.formValidationError" style="margin-left: 10px">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
<span class="small text-muted" style="margin-left: 5px;">App templates cannot be deployed as Swarm Mode services for the moment. You can still use them to quickly deploy containers on the Docker host.</span>
</span>
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
</div>
</div>
<!-- !actions -->

View File

@ -1,6 +1,6 @@
angular.module('templates', [])
.controller('TemplatesController', ['$scope', '$q', '$state', '$stateParams', '$anchorScroll', '$filter', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', 'ControllerDataPipeline', 'FormValidator',
function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication, ControllerDataPipeline, FormValidator) {
.controller('TemplatesController', ['$scope', '$q', '$state', '$stateParams', '$anchorScroll', '$filter', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', 'FormValidator',
function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication, FormValidator) {
$scope.state = {
selectedTemplate: null,
showAdvancedOptions: false,
@ -14,7 +14,8 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerSer
$scope.formValues = {
network: '',
name: ''
name: '',
AccessControlData: new AccessControlFormData()
};
$scope.addVolume = function () {
@ -49,7 +50,7 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerSer
$('#createContainerSpinner').show();
var userDetails = Authentication.getUserDetails();
var accessControlData = ControllerDataPipeline.getAccessControlFormData();
var accessControlData = $scope.formValues.AccessControlData;
var isAdmin = userDetails.role === 1 ? true : false;
if (!validateForm(accessControlData, isAdmin)) {

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="users">Users</a> > <a ui-sref="user({id: user.Id})">{{ user.Username }}</a>
<a ui-sref="users">Users</a> &gt; <a ui-sref="user({id: user.Id})">{{ user.Username }}</a>
</rd-header-content>
</rd-header>

View File

@ -69,7 +69,7 @@
Add to team(s)
</label>
<span class="small text-muted" style="margin-left: 20px;" ng-if="teams.length === 0">
You have not yet created any team. Head over the <a ui-sref="teams">teams view</a> to manage user teams.</span>
You have not yet created any team. Head over the <a ui-sref="teams">teams view</a> to manage user teams.
</span>
<span isteven-multi-select
ng-if="teams.length > 0"
@ -81,8 +81,8 @@
helper-elements="filter"
search-property="Name"
translation="{nothingSelected: 'Select one or more teams', search: 'Search...'}"
style="margin-left: 20px;"
</span>
style="margin-left: 20px;">
</span>
</div>
</div>
<!-- !teams -->
@ -183,6 +183,6 @@
</div>
</div>
</rd-widget-body>
<rd-widget>
</rd-widget>
</div>
</div>

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="volumes">Volumes</a> > <a ui-sref="volume({id: volume.Id})">{{ volume.Id }}</a>
<a ui-sref="volumes">Volumes</a> &gt; <a ui-sref="volume({id: volume.Id})">{{ volume.Id }}</a>
</rd-header-content>
</rd-header>
@ -47,7 +47,13 @@
</div>
</div>
<div ng-include="'app/components/common/accessControlPanel/accessControlPanel.html'" ng-if="volume && applicationState.application.authentication"></div>
<!-- access-control-panel -->
<por-access-control-panel
ng-if="volume && applicationState.application.authentication"
resource-control="volume.ResourceControl"
resource-type="'volume'">
</por-access-control-panel>
<!-- !access-control-panel -->
<div class="row" ng-if="!(volume.Options | emptyobject)">
<div class="col-lg-12 col-md-12 col-xs-12">

View File

@ -1,6 +1,6 @@
angular.module('volume', [])
.controller('VolumeController', ['$scope', '$state', '$stateParams', 'VolumeService', 'Notifications', 'ControllerDataPipeline',
function ($scope, $state, $stateParams, VolumeService, Notifications, ControllerDataPipeline) {
.controller('VolumeController', ['$scope', '$state', '$stateParams', 'VolumeService', 'Notifications',
function ($scope, $state, $stateParams, VolumeService, Notifications) {
$scope.removeVolume = function removeVolume() {
$('#loadingViewSpinner').show();
@ -22,7 +22,6 @@ function ($scope, $state, $stateParams, VolumeService, Notifications, Controller
VolumeService.volume($stateParams.id)
.then(function success(data) {
var volume = data;
ControllerDataPipeline.setAccessControlData('volume', volume.Id, volume.ResourceControl);
$scope.volume = volume;
})
.catch(function error(err) {

View File

@ -30,6 +30,17 @@
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
<span class="btn-group btn-group-sm pull-right" style="margin-right: 20px;">
<label class="btn btn-primary" ng-model="state.danglingVolumesOnly" uib-btn-radio="undefined">
All
</label>
<label class="btn btn-primary" ng-model="state.danglingVolumesOnly" uib-btn-radio="false">
Attached
</label>
<label class="btn btn-primary" ng-model="state.danglingVolumesOnly" uib-btn-radio="true">
Dangling
</label>
</span>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
@ -70,11 +81,14 @@
</tr>
</thead>
<tbody>
<tr dir-paginate="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<tr dir-paginate="volume in (state.filteredVolumes = (volumes | filter:{dangling: state.danglingVolumesOnly} | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="volume.Checked" ng-change="selectItem(volume)"/></td>
<td><a ui-sref="volume({id: volume.Id})">{{ volume.Id|truncate:25 }}</a></td>
<td>
<a ui-sref="volume({id: volume.Id})" class="monospaced">{{ volume.Id|truncate:25 }}</a>
<span style="margin-left: 10px;" class="label label-warning image-tag" ng-if="volume.dangling">Dangling</span></td>
</td>
<td>{{ volume.Driver }}</td>
<td>{{ volume.Mountpoint | truncate:52 }}</td>
<td>{{ volume.Mountpoint | truncatelr }}</td>
<td ng-if="applicationState.application.authentication">
<span>
<i ng-class="volume.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
@ -85,7 +99,7 @@
<tr ng-if="!volumes">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="volumes.length == 0">
<tr ng-if="volumes.length === 0 || state.filteredVolumes.length === 0">
<td colspan="5" class="text-center text-muted">No volumes available.</td>
</tr>
</tbody>
@ -95,5 +109,5 @@
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</rd-widget>
</div>

View File

@ -65,13 +65,29 @@ function ($q, $scope, VolumeService, Notifications, Pagination) {
function initView() {
$('#loadVolumesSpinner').show();
VolumeService.volumes()
.then(function success(data) {
$scope.volumes = data;
$q.all({
attached: VolumeService.volumes({
filters: {
'dangling': ['false']
}
}),
dangling: VolumeService.volumes({
filters: {
'dangling': ['true']
}
})
})
.catch(function error(err) {
.then(function success(data) {
$scope.volumes = data.attached.map(function(volume) {
volume.dangling = false;
return volume;
}).concat(data.dangling.map(function(volume) {
volume.dangling = true;
return volume;
}));
}).catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve volumes');
$scope.volumes = [];
})
.finally(function final() {
$('#loadVolumesSpinner').hide();

View File

@ -0,0 +1,12 @@
angular.module('portainer').component('porAccessControlForm', {
templateUrl: 'app/directives/accessControlForm/porAccessControlForm.html',
controller: 'porAccessControlFormController',
bindings: {
// This object will be populated with the form data.
// Model reference in porAccessControlFromModel.js
formData: '=',
// Optional. An existing resource control object that will be used to set
// the default values of the component.
resourceControl: '<'
}
});

View File

@ -0,0 +1,124 @@
<div>
<div class="col-sm-12 form-section-title">
Access control
</div>
<!-- access-control-switch -->
<div class="form-group">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Enable access control
<portainer-tooltip position="bottom" message="When enabled, you can restrict the access and management of this resource."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input name="ownership" type="checkbox" ng-model="$ctrl.formData.AccessControlEnabled"><i></i>
</label>
</div>
</div>
<!-- !access-control-switch -->
<!-- restricted-access -->
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div ng-if="$ctrl.isAdmin">
<input type="radio" id="access_administrators" ng-model="$ctrl.formData.Ownership" value="administrators">
<label for="access_administrators">
<div class="boxselector_header">
<i ng-class="'administrators' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Administrators
</div>
<p>I want to restrict the management of this resource to administrators only</p>
</label>
</div>
<div ng-if="$ctrl.isAdmin">
<input type="radio" id="access_restricted" ng-model="$ctrl.formData.Ownership" value="restricted">
<label for="access_restricted">
<div class="boxselector_header">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Restricted
</div>
<p>
I want to restrict the management of this resource to a set of users and/or teams
</p>
</label>
</div>
<div ng-if="!$ctrl.isAdmin">
<input type="radio" id="access_private" ng-model="$ctrl.formData.Ownership" value="private">
<label for="access_private">
<div class="boxselector_header">
<i ng-class="'private' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Private
</div>
<p>
I want to this resource to be manageable by myself only
</p>
</label>
</div>
<div ng-if="!$ctrl.isAdmin && $ctrl.availableTeams.length > 0">
<input type="radio" id="access_restricted" ng-model="$ctrl.formData.Ownership" value="restricted">
<label for="access_restricted">
<div class="boxselector_header">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Restricted
</div>
<p ng-if="$ctrl.availableTeams.length === 1">
I want any member of my team (<b>{{ $ctrl.availableTeams[0].Name }}</b>) to be able to manage this resource
</p>
<p ng-if="$ctrl.availableTeams.length > 1">
I want to restrict the management of this resource to one or more of my teams
</p>
</label>
</div>
</div>
</div>
<!-- restricted-access -->
<!-- authorized-teams -->
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === 'restricted' && ($ctrl.isAdmin || (!$ctrl.isAdmin && $ctrl.availableTeams.length > 1))" >
<div class="col-sm-12">
<label for="group-access" class="control-label text-left">
Authorized teams
<portainer-tooltip ng-if="$ctrl.isAdmin && $ctrl.availableTeams.length > 0" position="bottom" message="You can select which teams(s) will be able to manage this resource."></portainer-tooltip>
<portainer-tooltip ng-if="!$ctrl.isAdmin && $ctrl.availableTeams.length > 1" position="bottom" message="As you are a member of multiple teams, you can select which teams(s) will be able to manage this resource."></portainer-tooltip>
</label>
<span ng-if="$ctrl.isAdmin && $ctrl.availableTeams.length === 0" class="small text-muted" style="margin-left: 20px;">
You have not yet created any team. Head over the <a ui-sref="teams">teams view</a> to manage user teams.
</span>
<span isteven-multi-select
ng-if="($ctrl.isAdmin && $ctrl.availableTeams.length > 0) || (!$ctrl.isAdmin && $ctrl.availableTeams.length > 1)"
input-model="$ctrl.availableTeams"
output-model="$ctrl.formData.AuthorizedTeams"
button-label="Name"
item-label="Name"
tick-property="selected"
helper-elements="filter"
search-property="Name"
translation="{nothingSelected: 'Select one or more teams', search: 'Search...'}"
style="margin-left: 20px;">
</span>
</div>
</div>
<!-- !authorized-teams -->
<!-- authorized-users -->
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === 'restricted' && $ctrl.isAdmin">
<div class="col-sm-12">
<label for="group-access" class="control-label text-left">
Authorized users
<portainer-tooltip ng-if="$ctrl.isAdmin && $ctrl.availableUsers.length > 0" position="bottom" message="You can select which user(s) will be able to manage this resource."></portainer-tooltip>
</label>
<span ng-if="$ctrl.availableUsers.length === 0" class="small text-muted" style="margin-left: 20px;">
You have not yet created any user. Head over the <a ui-sref="users">users view</a> to manage users.
</span>
<span isteven-multi-select
ng-if="$ctrl.availableUsers.length > 0"
input-model="$ctrl.availableUsers"
output-model="$ctrl.formData.AuthorizedUsers"
button-label="Username"
item-label="Username"
tick-property="selected"
helper-elements="filter"
search-property="Username"
translation="{nothingSelected: 'Select one or more users', search: 'Search...'}"
style="margin-left: 20px;">
</span>
</div>
</div>
<!-- !authorized-users -->
</div>

View File

@ -0,0 +1,76 @@
angular.module('portainer')
.controller('porAccessControlFormController', ['$q', 'UserService', 'Notifications', 'Authentication', 'ResourceControlService',
function ($q, UserService, Notifications, Authentication, ResourceControlService) {
var ctrl = this;
ctrl.availableTeams = [];
ctrl.availableUsers = [];
function setOwnership(resourceControl, isAdmin) {
if (isAdmin && resourceControl.Ownership === 'private') {
ctrl.formData.Ownership = 'restricted';
} else {
ctrl.formData.Ownership = resourceControl.Ownership;
}
}
function setAuthorizedUsersAndTeams(authorizedUsers, authorizedTeams) {
angular.forEach(ctrl.availableUsers, function(user) {
var found = _.find(authorizedUsers, { Id: user.Id });
if (found) {
user.selected = true;
}
});
angular.forEach(ctrl.availableTeams, function(team) {
var found = _.find(authorizedTeams, { Id: team.Id });
if (found) {
team.selected = true;
}
});
}
function initComponent() {
$('#loadingViewSpinner').show();
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true: false;
ctrl.isAdmin = isAdmin;
if (isAdmin) {
ctrl.formData.Ownership = 'administrators';
}
$q.all({
availableTeams: UserService.userTeams(userDetails.ID),
availableUsers: isAdmin ? UserService.users(false) : []
})
.then(function success(data) {
ctrl.availableUsers = data.availableUsers;
var availableTeams = data.availableTeams;
ctrl.availableTeams = availableTeams;
if (!isAdmin && availableTeams.length === 1) {
ctrl.formData.AuthorizedTeams = availableTeams;
}
return $q.when(ctrl.resourceControl && ResourceControlService.retrieveOwnershipDetails(ctrl.resourceControl));
})
.then(function success(data) {
if (data) {
var authorizedUsers = data.authorizedUsers;
var authorizedTeams = data.authorizedTeams;
setOwnership(ctrl.resourceControl, isAdmin);
setAuthorizedUsersAndTeams(authorizedUsers, authorizedTeams);
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve access control information');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
initComponent();
}]);

View File

@ -0,0 +1,6 @@
function AccessControlFormData() {
this.AccessControlEnabled = true;
this.Ownership = 'private';
this.AuthorizedUsers = [];
this.AuthorizedTeams = [];
}

View File

@ -0,0 +1,12 @@
angular.module('portainer').component('porAccessControlPanel', {
templateUrl: 'app/directives/accessControlPanel/porAccessControlPanel.html',
controller: 'porAccessControlPanelController',
bindings: {
// The component will display information about this resource control object.
resourceControl: '=',
// This component is usually displayed inside a resource-details view.
// This variable specifies the type of the associated resource.
// Accepted values: 'container', 'service' or 'volume'.
resourceType: '<'
}
});

View File

@ -1,5 +1,5 @@
<div class="row" ng-controller="AccessControlPanelController">
<div class="col-sm-12" ng-if="state.displayAccessControlPanel">
<div class="row">
<div class="col-sm-12" ng-if="$ctrl.state.displayAccessControlPanel">
<rd-widget>
<rd-widget-header icon="fa-eye" title="Access control"></rd-widget-header>
<rd-widget-body classes="no-padding">
@ -9,63 +9,63 @@
<tr>
<td>Ownership</td>
<td>
<i ng-class="resourceControl.Ownership | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
<span ng-if="!resourceControl">
<i ng-class="$ctrl.resourceControl.Ownership | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
<span ng-if="!$ctrl.resourceControl">
public
<portainer-tooltip message="This resource can be managed by any user with access to this endpoint." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
</span>
<span ng-if="resourceControl">
{{ resourceControl.Ownership }}
<portainer-tooltip ng-if="resourceControl.Ownership === 'administrators'" message="This resource can only be managed by administrators." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
<portainer-tooltip ng-if="resourceControl.Ownership === 'private'" message="Management of this resource is restricted to a single user." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
<portainer-tooltip ng-if="resourceControl.Ownership === 'restricted'" message="This resource can be managed by a restricted set of users and/or teams." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
<span ng-if="$ctrl.resourceControl">
{{ $ctrl.resourceControl.Ownership }}
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === 'administrators'" message="This resource can only be managed by administrators." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === 'private'" message="Management of this resource is restricted to a single user." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === 'restricted'" message="This resource can be managed by a restricted set of users and/or teams." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
</span>
</td>
</tr>
<!-- !ownership -->
<tr ng-if="resourceControl.Type === 2 && resourceType === 'container'">
<tr ng-if="$ctrl.resourceControl.Type === 2 && $ctrl.resourceType === 'container'">
<td colspan="2">
<i class="fa fa-info-circle" aria-hidden="true" style="margin-right: 2px;"></i>
Access control on this resource is inherited from the following service: <a ui-sref="service({ id: resourceControl.ResourceId })">{{ resourceControl.ResourceId | truncate }}</a>
Access control on this resource is inherited from the following service: <a ui-sref="service({ id: $ctrl.resourceControl.ResourceId })">{{ $ctrl.resourceControl.ResourceId | truncate }}</a>
<portainer-tooltip message="Access control applied on a service is also applied on each container of that service." position="bottom" style="margin-left: 2px;"></portainer-tooltip>
</td>
</tr>
<tr ng-if="resourceControl.Type === 1 && resourceType === 'volume'">
<tr ng-if="$ctrl.resourceControl.Type === 1 && $ctrl.resourceType === 'volume'">
<td colspan="2">
<i class="fa fa-info-circle" aria-hidden="true" style="margin-right: 2px;"></i>
Access control on this resource is inherited from the following container: <a ui-sref="container({ id: resourceControl.ResourceId })">{{ resourceControl.ResourceId | truncate }}</a>
Access control on this resource is inherited from the following container: <a ui-sref="container({ id: $ctrl.resourceControl.ResourceId })">{{ $ctrl.resourceControl.ResourceId | truncate }}</a>
<portainer-tooltip message="Access control applied on a container created using a template is also applied on each volume associated to the container." position="bottom" style="margin-left: 2px;"></portainer-tooltip>
</td>
</tr>
<!-- authorized-users -->
<tr ng-if="resourceControl.UserAccesses.length > 0">
<tr ng-if="$ctrl.resourceControl.UserAccesses.length > 0">
<td>Authorized users</td>
<td>
<span ng-repeat="user in authorizedUsers">{{user.Username}}{{$last ? '' : ', '}} </span>
<span ng-repeat="user in $ctrl.authorizedUsers">{{user.Username}}{{$last ? '' : ', '}} </span>
</td>
</tr>
<!-- !authorized-users -->
<!-- authorized-teams -->
<tr ng-if="resourceControl.TeamAccesses.length > 0">
<tr ng-if="$ctrl.resourceControl.TeamAccesses.length > 0">
<td>Authorized teams</td>
<td>
<span ng-repeat="team in authorizedTeams">{{team.Name}}{{$last ? '' : ', '}} </span>
<span ng-repeat="team in $ctrl.authorizedTeams">{{team.Name}}{{$last ? '' : ', '}} </span>
</td>
</tr>
<!-- !authorized-teams -->
<!-- edit-ownership -->
<tr ng-if="!(resourceControl.Type === 1 && resourceType === 'volume') && !(resourceControl.Type === 2 && resourceType === 'container') && !state.editOwnership && (isAdmin || state.canEditOwnership)">
<tr ng-if="!($ctrl.resourceControl.Type === 1 && $ctrl.resourceType === 'volume') && !($ctrl.resourceControl.Type === 2 && $ctrl.resourceType === 'container') && !$ctrl.state.editOwnership && ($ctrl.isAdmin || $ctrl.state.canEditOwnership)">
<td colspan="2">
<a class="btn-outline-secondary" ng-click="state.editOwnership = true"><i class="fa fa-edit space-right" aria-hidden="true"></i>Change ownership</a>
<a class="btn-outline-secondary" ng-click="$ctrl.state.editOwnership = true"><i class="fa fa-edit space-right" aria-hidden="true"></i>Change ownership</a>
</td>
</tr>
<!-- !edit-ownership -->
<!-- edit-ownership-choices -->
<tr ng-if="state.editOwnership">
<tr ng-if="$ctrl.state.editOwnership">
<td colspan="2">
<div class="boxselector_wrapper">
<div ng-if="isAdmin">
<input type="radio" id="access_administrators" ng-model="formValues.Ownership" value="administrators">
<div ng-if="$ctrl.isAdmin">
<input type="radio" id="access_administrators" ng-model="$ctrl.formValues.Ownership" value="administrators">
<label for="access_administrators">
<div class="boxselector_header">
<i ng-class="'administrators' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
@ -74,8 +74,8 @@
<p>I want to restrict the management of this resource to administrators only</p>
</label>
</div>
<div ng-if="isAdmin">
<input type="radio" id="access_restricted" ng-model="formValues.Ownership" value="restricted">
<div ng-if="$ctrl.isAdmin">
<input type="radio" id="access_restricted" ng-model="$ctrl.formValues.Ownership" value="restricted">
<label for="access_restricted">
<div class="boxselector_header">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
@ -86,23 +86,23 @@
</p>
</label>
</div>
<div ng-if="!isAdmin && state.canChangeOwnershipToTeam && availableTeams.length > 0">
<input type="radio" id="access_restricted" ng-model="formValues.Ownership" value="restricted">
<div ng-if="!$ctrl.isAdmin && $ctrl.state.canChangeOwnershipToTeam && $ctrl.availableTeams.length > 0">
<input type="radio" id="access_restricted" ng-model="$ctrl.formValues.Ownership" value="restricted">
<label for="access_restricted">
<div class="boxselector_header">
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
Restricted
</div>
<p ng-if="availableTeams.length === 1">
I want any member of my team (<b>{{ availableTeams[0].Name }}</b>) to be able to manage this resource
<p ng-if="$ctrl.availableTeams.length === 1">
I want any member of my team (<b>{{ $ctrl.availableTeams[0].Name }}</b>) to be able to manage this resource
</p>
<p ng-if="availableTeams.length > 1">
<p ng-if="$ctrl.availableTeams.length > 1">
I want to restrict the management of this resource to one or more of my teams
</p>
</label>
</div>
<div>
<input type="radio" id="access_public" ng-model="formValues.Ownership" value="public">
<input type="radio" id="access_public" ng-model="$ctrl.formValues.Ownership" value="public">
<label for="access_public">
<div class="boxselector_header">
<i ng-class="'public' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
@ -116,56 +116,56 @@
</tr>
<!-- edit-ownership-choices -->
<!-- select-teams -->
<tr ng-if="state.editOwnership && formValues.Ownership === 'restricted' && (isAdmin || !isAdmin && availableTeams.length > 1)">
<tr ng-if="$ctrl.state.editOwnership && $ctrl.formValues.Ownership === 'restricted' && ($ctrl.isAdmin || !$ctrl.isAdmin && $ctrl.availableTeams.length > 1)">
<td colspan="2">
<span>Teams</span>
<span ng-if="isAdmin && availableTeams.length === 0" class="small text-muted" style="margin-left: 10px;">
You have not yet created any team. Head over the <a ui-sref="teams">teams view</a> to manage user teams.</span>
<span ng-if="$ctrl.isAdmin && $ctrl.availableTeams.length === 0" class="small text-muted" style="margin-left: 10px;">
You have not yet created any team. Head over the <a ui-sref="teams">teams view</a> to manage user teams.
</span>
<span isteven-multi-select
ng-if="(isAdmin && availableTeams.length > 0) || (!isAdmin && availableTeams.length > 1)"
input-model="availableTeams"
output-model="formValues.Ownership_Teams"
ng-if="($ctrl.isAdmin && $ctrl.availableTeams.length > 0) || (!$ctrl.isAdmin && $ctrl.availableTeams.length > 1)"
input-model="$ctrl.availableTeams"
output-model="$ctrl.formValues.Ownership_Teams"
button-label="Name"
item-label="Name"
tick-property="selected"
helper-elements="filter"
search-property="Name"
max-labels="3"
translation="{nothingSelected: 'Select one or more teams', search: 'Search...'}"
translation="{nothingSelected: 'Select one or more teams', search: 'Search...'}">
</span>
</td>
</tr>
<!-- !select-teams -->
<!-- select-users -->
<tr ng-if="isAdmin && state.editOwnership && formValues.Ownership === 'restricted'">
<tr ng-if="$ctrl.isAdmin && $ctrl.state.editOwnership && $ctrl.formValues.Ownership === 'restricted'">
<td colspan="2">
<span>Users</span>
<span ng-if="availableUsers.length === 0" class="small text-muted" style="margin-left: 10px;">
You have not yet created any user. Head over the <a ui-sref="users">users view</a> to manage users.</span>
<span ng-if="$ctrl.availableUsers.length === 0" class="small text-muted" style="margin-left: 10px;">
You have not yet created any user. Head over the <a ui-sref="users">users view</a> to manage users.
</span>
<span isteven-multi-select
ng-if="availableUsers.length > 0"
input-model="availableUsers"
output-model="formValues.Ownership_Users"
ng-if="$ctrl.availableUsers.length > 0"
input-model="$ctrl.availableUsers"
output-model="$ctrl.formValues.Ownership_Users"
button-label="Username"
item-label="Username"
tick-property="selected"
helper-elements="filter"
search-property="Username"
max-labels="3"
translation="{nothingSelected: 'Select one or more users', search: 'Search...'}"
translation="{nothingSelected: 'Select one or more users', search: 'Search...'}">
</span>
</td>
</tr>
<!-- !select-users -->
<!-- ownership-actions -->
<tr ng-if="state.editOwnership">
<tr ng-if="$ctrl.state.editOwnership">
<td colspan="2">
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="state.editOwnership = false">Cancel</a>
<a type="button" class="btn btn-primary btn-sm" ng-click="confirmUpdateOwnership()">Update ownership</a>
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.state.editOwnership = false">Cancel</a>
<a type="button" class="btn btn-primary btn-sm" ng-click="$ctrl.confirmUpdateOwnership()">Update ownership</a>
<span class="text-danger" ng-if="$ctrl.state.formValidationError" style="margin-left: 5px;">{{ $ctrl.state.formValidationError }}</span>
</div>
</td>
</tr>

View File

@ -0,0 +1,156 @@
angular.module('portainer')
.controller('porAccessControlPanelController', ['$q', '$state', 'UserService', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'FormValidator',
function ($q, $state, UserService, ResourceControlService, Notifications, Authentication, ModalService, FormValidator) {
var ctrl = this;
ctrl.state = {
displayAccessControlPanel: false,
canEditOwnership: false,
editOwnership: false,
formValidationError: ''
};
ctrl.formValues = {
Ownership: 'public',
Ownership_Users: [],
Ownership_Teams: []
};
ctrl.authorizedUsers = [];
ctrl.availableUsers = [];
ctrl.authorizedTeams = [];
ctrl.availableTeams = [];
ctrl.confirmUpdateOwnership = function (force) {
if (!validateForm()) {
return;
}
ModalService.confirmAccessControlUpdate(function (confirmed) {
if(!confirmed) { return; }
updateOwnership();
});
};
function validateForm() {
ctrl.state.formValidationError = '';
var error = '';
var accessControlData = {
AccessControlEnabled: ctrl.formValues.Ownership === 'public' ? false : true,
Ownership: ctrl.formValues.Ownership,
AuthorizedUsers: ctrl.formValues.Ownership_Users,
AuthorizedTeams: ctrl.formValues.Ownership_Teams
};
var isAdmin = ctrl.isAdmin;
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
if (error) {
ctrl.state.formValidationError = error;
return false;
}
return true;
}
function processOwnershipFormValues() {
var userIds = [];
angular.forEach(ctrl.formValues.Ownership_Users, function(user) {
userIds.push(user.Id);
});
var teamIds = [];
angular.forEach(ctrl.formValues.Ownership_Teams, function(team) {
teamIds.push(team.Id);
});
var administratorsOnly = ctrl.formValues.Ownership === 'administrators' ? true : false;
return {
ownership: ctrl.formValues.Ownership,
authorizedUserIds: administratorsOnly ? [] : userIds,
authorizedTeamIds: administratorsOnly ? [] : teamIds,
administratorsOnly: administratorsOnly
};
}
function updateOwnership() {
$('#loadingViewSpinner').show();
var resourceId = ctrl.resourceControl.ResourceId;
var ownershipParameters = processOwnershipFormValues();
ResourceControlService.applyResourceControlChange(ctrl.resourceType, resourceId,
ctrl.resourceControl, ownershipParameters)
.then(function success(data) {
Notifications.success('Access control successfully updated');
$state.reload();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update access control');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
function initComponent() {
$('#loadingViewSpinner').show();
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true: false;
var userId = userDetails.ID;
ctrl.isAdmin = isAdmin;
var resourceControl = ctrl.resourceControl;
if (isAdmin) {
if (resourceControl) {
ctrl.formValues.Ownership = resourceControl.Ownership === 'private' ? 'restricted' : resourceControl.Ownership;
} else {
ctrl.formValues.Ownership = 'public';
}
} else {
ctrl.formValues.Ownership = 'public';
}
ResourceControlService.retrieveOwnershipDetails(resourceControl)
.then(function success(data) {
ctrl.authorizedUsers = data.authorizedUsers;
ctrl.authorizedTeams = data.authorizedTeams;
return ResourceControlService.retrieveUserPermissionsOnResource(userId, isAdmin, resourceControl);
})
.then(function success(data) {
ctrl.state.canEditOwnership = data.isPartOfRestrictedUsers || data.isLeaderOfAnyRestrictedTeams;
ctrl.state.canChangeOwnershipToTeam = data.isPartOfRestrictedUsers;
return $q.all({
availableUsers: isAdmin ? UserService.users(false) : [],
availableTeams: isAdmin || data.isPartOfRestrictedUsers ? UserService.userTeams(userId) : []
});
})
.then(function success(data) {
ctrl.availableUsers = data.availableUsers;
angular.forEach(ctrl.availableUsers, function(user) {
var found = _.find(ctrl.authorizedUsers, { Id: user.Id });
if (found) {
user.selected = true;
}
});
ctrl.availableTeams = data.availableTeams;
angular.forEach(data.availableTeams, function(team) {
var found = _.find(ctrl.authorizedTeams, { Id: team.Id });
if (found) {
team.selected = true;
}
});
if (data.availableTeams.length === 1) {
ctrl.formValues.Ownership_Teams.push(data.availableTeams[0]);
}
ctrl.state.displayAccessControlPanel = true;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve access control information');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
initComponent();
}]);

View File

@ -57,7 +57,7 @@ function (AccessService, Pagination, Notifications) {
function removeFromAccesses(access, accesses) {
_.remove(accesses, function(n) {
return n.Id === access.Id;
return n.Id === access.Id && n.Type === access.Type;
});
}

View File

@ -6,7 +6,7 @@ angular
message: '@',
position: '@'
},
template: '<span class="interactive" tooltip-placement="{{position}}" tooltip-class="portainer-tooltip" uib-tooltip="{{message}}"><i class="fa fa-question-circle tooltip-icon" aria-hidden="true"></i></span>',
template: '<span class="interactive" tooltip-append-to-body="true" tooltip-placement="{{position}}" tooltip-class="portainer-tooltip" uib-tooltip="{{message}}"><i class="fa fa-question-circle tooltip-icon" aria-hidden="true"></i></span>',
restrict: 'E'
};
return directive;

View File

@ -12,12 +12,25 @@ angular.module('portainer.filters', [])
if (text.length <= length || text.length - end.length <= length) {
return text;
}
else {
} else {
return String(text).substring(0, length - end.length) + end;
}
};
})
.filter('truncatelr', function () {
'use strict';
return function (text, max, left, right) {
max = isNaN(max) ? 50 : max;
left = isNaN(left) ? 25 : left;
right = isNaN(right) ? 25 : right;
if (text.length <= max) {
return text;
} else {
return text.substring(0, left) + '[...]' + text.substring(text.length - right, text.length);
}
};
})
.filter('taskstatusbadge', function () {
'use strict';
return function (text) {
@ -271,4 +284,10 @@ angular.module('portainer.filters', [])
}
return '';
};
})
.filter('imagelayercommand', function () {
'use strict';
return function (createdBy) {
return createdBy.replace('/bin/sh -c #(nop) ', '').replace('/bin/sh -c ', 'RUN ');
};
});

View File

@ -4,25 +4,25 @@ angular.module('portainer.helpers')
var helper = {};
helper.retrieveAuthorizedUsers = function(resourceControl, users) {
var authorizedUserNames = [];
var authorizedUsers = [];
angular.forEach(resourceControl.UserAccesses, function(access) {
var user = _.find(users, { Id: access.UserId });
if (user) {
authorizedUserNames.push(user);
authorizedUsers.push(user);
}
});
return authorizedUserNames;
return authorizedUsers;
};
helper.retrieveAuthorizedTeams = function(resourceControl, teams) {
var authorizedTeamNames = [];
var authorizedTeams = [];
angular.forEach(resourceControl.TeamAccesses, function(access) {
var team = _.find(teams, { Id: access.TeamId });
if (team) {
authorizedTeamNames.push(team);
authorizedTeams.push(team);
}
});
return authorizedTeamNames;
return authorizedTeams;
};
helper.isLeaderOfAnyRestrictedTeams = function(userMemberships, resourceControl) {

View File

@ -1,5 +1,4 @@
angular.module('portainer.helpers')
.factory('ServiceHelper', [function ServiceHelperFactory() {
angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHelperFactory() {
'use strict';
return {
serviceToConfig: function(service) {
@ -13,17 +12,113 @@ angular.module('portainer.helpers')
EndpointSpec: service.Spec.EndpointSpec
};
},
translateKeyValueToPlacementPreferences: function(keyValuePreferences) {
if (keyValuePreferences) {
var preferences = [];
keyValuePreferences.forEach(function(preference) {
if (preference.strategy && preference.strategy !== '' && preference.value && preference.value !== '') {
switch (preference.strategy.toLowerCase()) {
case 'spread':
preferences.push({
'Spread': {
'SpreadDescriptor': preference.value
}
});
break;
}
}
});
return preferences;
}
return [];
},
translateKeyValueToPlacementConstraints: function(keyValueConstraints) {
if (keyValueConstraints) {
var constraints = [];
keyValueConstraints.forEach(function(keyValueConstraint) {
if (keyValueConstraint.key && keyValueConstraint.key !== '' && keyValueConstraint.value && keyValueConstraint.value !== '') {
constraints.push(keyValueConstraint.key + keyValueConstraint.operator + keyValueConstraint.value);
keyValueConstraints.forEach(function(constraint) {
if (constraint.key && constraint.key !== '' && constraint.value && constraint.value !== '') {
constraints.push(constraint.key + constraint.operator + constraint.value);
}
});
return constraints;
}
return [];
}
},
translateEnvironmentVariables: function(env) {
if (env) {
var variables = [];
env.forEach(function(variable) {
var idx = variable.indexOf('=');
var keyValue = [variable.slice(0, idx), variable.slice(idx + 1)];
var originalValue = (keyValue.length > 1) ? keyValue[1] : '';
variables.push({
key: keyValue[0],
value: originalValue,
originalKey: keyValue[0],
originalValue: originalValue,
added: true
});
});
return variables;
}
return [];
},
translateEnvironmentVariablesToEnv: function(env) {
if (env) {
var variables = [];
env.forEach(function(variable) {
if (variable.key && variable.key !== '') {
variables.push(variable.key + '=' + variable.value);
}
});
return variables;
}
return [];
},
translatePreferencesToKeyValue: function(preferences) {
if (preferences) {
var keyValuePreferences = [];
preferences.forEach(function(preference) {
if (preference.Spread) {
keyValuePreferences.push({
strategy: 'Spread',
value: preference.Spread.SpreadDescriptor
});
}
});
return keyValuePreferences;
}
return [];
},
translateConstraintsToKeyValue: function(constraints) {
function getOperator(constraint) {
var indexEquals = constraint.indexOf('==');
if (indexEquals >= 0) {
return [indexEquals, '=='];
}
return [constraint.indexOf('!='), '!='];
}
if (constraints) {
var keyValueConstraints = [];
constraints.forEach(function(constraint) {
var operatorIndices = getOperator(constraint);
var key = constraint.slice(0, operatorIndices[0]);
var operator = operatorIndices[1];
var value = constraint.slice(operatorIndices[0] + 2);
keyValueConstraints.push({
key: key,
value: value,
operator: operator,
originalKey: key,
originalValue: value
});
});
return keyValueConstraints;
}
return [];
}
};
}]);

View File

@ -3,6 +3,7 @@ function ImageViewModel(data) {
this.Tag = data.Tag;
this.Repository = data.Repository;
this.Created = data.Created;
this.Containers = data.dataUsage ? data.dataUsage.Containers : 0;
this.Checked = false;
this.RepoTags = data.RepoTags;
this.VirtualSize = data.VirtualSize;

View File

@ -0,0 +1,8 @@
function ImageLayerViewModel(data) {
this.Id = data.Id;
this.Created = data.Created;
this.CreatedBy = data.CreatedBy;
this.Size = data.Size;
this.Comment = data.Comment;
this.Tags = data.Tags;
}

View File

@ -41,27 +41,37 @@ function ServiceViewModel(data, runningTasks, nodes) {
this.RestartWindow = 0;
}
this.Constraints = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Constraints || [] : [];
this.Preferences = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Preferences || [] : [];
this.Platforms = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Platforms || [] : [];
this.Labels = data.Spec.Labels;
var containerSpec = data.Spec.TaskTemplate.ContainerSpec;
if (containerSpec) {
this.ContainerLabels = containerSpec.Labels;
this.Env = containerSpec.Env;
this.Mounts = containerSpec.Mounts || [];
this.User = containerSpec.User;
this.Dir = containerSpec.Dir;
this.Command = containerSpec.Command;
this.Arguments = containerSpec.Args;
this.Hostname = containerSpec.Hostname;
this.Env = containerSpec.Env;
this.Dir = containerSpec.Dir;
this.User = containerSpec.User;
this.Groups = containerSpec.Groups;
this.TTY = containerSpec.TTY;
this.OpenStdin = containerSpec.OpenStdin;
this.ReadOnly = containerSpec.ReadOnly;
this.Mounts = containerSpec.Mounts || [];
this.StopSignal = containerSpec.StopSignal;
this.StopGracePeriod = containerSpec.StopGracePeriod;
this.HealthCheck = containerSpec.HealthCheck || {};
this.Hosts = containerSpec.Hosts;
this.DNSConfig = containerSpec.DNSConfig;
this.Secrets = containerSpec.Secrets;
}
if (data.Endpoint) {
this.Ports = data.Endpoint.Ports;
}
this.Mounts = [];
if (data.Spec.TaskTemplate.ContainerSpec.Mounts) {
this.Mounts = data.Spec.TaskTemplate.ContainerSpec.Mounts;
}
this.LogDriver = data.Spec.TaskTemplate.LogDriver;
this.Runtime = data.Spec.TaskTemplate.Runtime;
this.VirtualIPs = data.Endpoint ? data.Endpoint.VirtualIPs : [];
@ -75,6 +85,8 @@ function ServiceViewModel(data, runningTasks, nodes) {
this.UpdateFailureAction = 'pause';
}
this.RollbackConfig = data.Spec.RollbackConfig;
this.Checked = false;
this.Scale = false;
this.EditName = false;

View File

@ -0,0 +1,20 @@
angular.module('portainer.rest')
.factory('ServiceLogs', ['$http', 'DOCKER_ENDPOINT', 'EndpointProvider', function ServiceLogsFactory($http, DOCKER_ENDPOINT, EndpointProvider) {
'use strict';
return {
get: function (id, params, callback) {
$http({
method: 'GET',
url: DOCKER_ENDPOINT + '/' + EndpointProvider.endpointID() + '/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(error, data);
});
}
};
}]);

View File

@ -12,6 +12,7 @@ angular.module('portainer.rest')
method: 'GET', params: { action: 'events', since: '@since', until: '@until' },
isArray: true, transformResponse: jsonObjectsToArrayHandler
},
auth: { method: 'POST', params: { action: 'auth' } }
auth: { method: 'POST', params: { action: 'auth' } },
dataUsage: { method: 'GET', params: { action: 'system/df' } }
});
}]);

View File

@ -29,14 +29,14 @@ angular.module('portainer.services')
};
service.applyResourceControl = function(resourceControlType, resourceIdentifier, userId, accessControlData, subResources) {
if (!accessControlData.accessControlEnabled) {
if (!accessControlData.AccessControlEnabled) {
return;
}
var authorizedUserIds = [];
var authorizedTeamIds = [];
var administratorsOnly = false;
switch (accessControlData.ownership) {
switch (accessControlData.Ownership) {
case 'administrators':
administratorsOnly = true;
break;
@ -44,10 +44,10 @@ angular.module('portainer.services')
authorizedUserIds.push(userId);
break;
case 'restricted':
angular.forEach(accessControlData.authorizedUsers, function(user) {
angular.forEach(accessControlData.AuthorizedUsers, function(user) {
authorizedUserIds.push(user.Id);
});
angular.forEach(accessControlData.authorizedTeams, function(team) {
angular.forEach(accessControlData.AuthorizedTeams, function(team) {
authorizedTeamIds.push(team.Id);
});
break;
@ -83,9 +83,9 @@ angular.module('portainer.services')
teams: resourceControl.TeamAccesses.length > 0 ? TeamService.teams() : []
})
.then(function success(data) {
var authorizedUserNames = ResourceControlHelper.retrieveAuthorizedUsers(resourceControl, data.users);
var authorizedTeamNames = ResourceControlHelper.retrieveAuthorizedTeams(resourceControl, data.teams);
deferred.resolve({ authorizedUsers: authorizedUserNames, authorizedTeams: authorizedTeamNames });
var authorizedUsers = ResourceControlHelper.retrieveAuthorizedUsers(resourceControl, data.users);
var authorizedTeams = ResourceControlHelper.retrieveAuthorizedTeams(resourceControl, data.teams);
deferred.resolve({ authorizedUsers: authorizedUsers, authorizedTeams: authorizedTeams });
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve user and team information', err: err });

View File

@ -1,36 +0,0 @@
// ControllerDataPipeline is used to transfer data between multiple controllers.
angular.module('portainer.services')
.factory('ControllerDataPipeline', [function ControllerDataPipelineFactory() {
'use strict';
var pipeline = {};
// accessControlData is used to manage the data required by the accessControlPanelController.
var accessControlData = {};
pipeline.setAccessControlData = function (type, resourceId, resourceControl) {
accessControlData.resourceType = type;
accessControlData.resourceId = resourceId;
accessControlData.resourceControl = resourceControl;
};
pipeline.getAccessControlData = function() {
return accessControlData;
};
// accessControlFormData is used to manage the data available in the scope of the accessControlFormController.
var accessControlFormData = {};
pipeline.setAccessControlFormData = function(accessControlEnabled, ownership, authorizedUsers, authorizedTeams) {
accessControlFormData.accessControlEnabled = accessControlEnabled;
accessControlFormData.ownership = ownership;
accessControlFormData.authorizedUsers = authorizedUsers;
accessControlFormData.authorizedTeams = authorizedTeams;
};
pipeline.getAccessControlFormData = function() {
return accessControlFormData;
};
return pipeline;
}]);

View File

@ -87,5 +87,23 @@ angular.module('portainer.services')
return deferred.promise;
};
service.createExec = function(execConfig) {
var deferred = $q.defer();
Container.exec(execConfig).$promise
.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message, err: data.message });
} else {
deferred.resolve(data);
}
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
return service;
}]);

View File

@ -0,0 +1,27 @@
angular.module('portainer.services')
.factory('ExecService', ['$q', '$timeout', 'Exec', function ExecServiceFactory($q, $timeout, Exec) {
'use strict';
var service = {};
service.resizeTTY = function(execId, height, width, timeout) {
var deferred = $q.defer();
$timeout(function() {
Exec.resize({id: execId, height: height, width: width}).$promise
.then(function success(data) {
if (data.message) {
deferred.reject({ msg: 'Unable to exec into container', err: data.message });
} else {
deferred.resolve(data);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to exec into container', err: err });
});
}, timeout);
return deferred.promise;
};
return service;
}]);

View File

@ -1,5 +1,5 @@
angular.module('portainer.services')
.factory('ImageService', ['$q', 'Image', 'ImageHelper', 'RegistryService', 'HttpRequestHelper', function ImageServiceFactory($q, Image, ImageHelper, RegistryService, HttpRequestHelper) {
.factory('ImageService', ['$q', 'Image', 'ImageHelper', 'RegistryService', 'HttpRequestHelper', 'SystemService', function ImageServiceFactory($q, Image, ImageHelper, RegistryService, HttpRequestHelper, SystemService) {
'use strict';
var service = {};
@ -20,11 +20,19 @@ angular.module('portainer.services')
return deferred.promise;
};
service.images = function() {
service.images = function(withUsage) {
var deferred = $q.defer();
Image.query({}).$promise
$q.all({
dataUsage: withUsage ? SystemService.dataUsage() : { Images: [] },
images: Image.query({}).$promise
})
.then(function success(data) {
var images = data.map(function (item) {
var images = data.images.map(function(item) {
item.dataUsage = data.dataUsage.Images.find(function(usage) {
return item.Id === usage.Id;
});
return new ImageViewModel(item);
});
deferred.resolve(images);
@ -35,6 +43,26 @@ angular.module('portainer.services')
return deferred.promise;
};
service.history = function(imageId) {
var deferred = $q.defer();
Image.history({id: imageId}).$promise
.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message });
} else {
var layers = [];
angular.forEach(data, function(imageLayer) {
layers.push(new ImageLayerViewModel(imageLayer));
});
deferred.resolve(layers);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve image details', err: err });
});
return deferred.promise;
};
service.pushImage = function(tag, registry) {
var deferred = $q.defer();

View File

@ -36,7 +36,21 @@ angular.module('portainer.services')
};
service.remove = function(secretId) {
return Secret.remove({ id: secretId }).$promise;
var deferred = $q.defer();
Secret.remove({ id: secretId }).$promise
.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to remove secret', err: err });
});
return deferred.promise;
};
service.create = function(secretConfig) {

View File

@ -40,6 +40,10 @@ angular.module('portainer.services')
return deferred.promise;
};
service.dataUsage = function () {
return System.dataUsage().$promise;
};
return service;
}]);

View File

@ -3,9 +3,9 @@ angular.module('portainer.services')
'use strict';
var service = {};
service.volumes = function() {
service.volumes = function(params) {
var deferred = $q.defer();
Volume.query().$promise
Volume.query(params).$promise
.then(function success(data) {
var volumes = data.Volumes || [];
volumes = volumes.map(function (item) {

View File

@ -5,16 +5,16 @@ angular.module('portainer.services')
var validator = {};
validator.validateAccessControl = function(accessControlData, isAdmin) {
if (!accessControlData.accessControlEnabled) {
if (!accessControlData.AccessControlEnabled) {
return '';
}
if (isAdmin && accessControlData.ownership === 'restricted' &&
accessControlData.authorizedUsers.length === 0 &&
accessControlData.authorizedTeams.length === 0) {
if (isAdmin && accessControlData.Ownership === 'restricted' &&
accessControlData.AuthorizedUsers.length === 0 &&
accessControlData.AuthorizedTeams.length === 0) {
return 'You must specify at least one team or user.';
} else if (!isAdmin && accessControlData.ownership === 'restricted' &&
accessControlData.authorizedTeams.length === 0) {
} else if (!isAdmin && accessControlData.Ownership === 'restricted' &&
accessControlData.AuthorizedTeams.length === 0) {
return 'You must specify at least a team.';
}
return '';

View File

@ -296,6 +296,14 @@ ul.sidebar .sidebar-list a.active {
background: #2d3e63;
}
#image-layers .btn{
padding: 0;
}
#image-layers .expand{
padding-right: 0;
}
ul.sidebar .sidebar-list .sidebar-sublist a {
text-indent: 35px;
font-size: 12px;
@ -505,3 +513,8 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
opacity: 0.9;
}
/*!toaster override*/
.monospaced {
font-family: monospace;
font-weight: 600;
}

View File

@ -1,6 +1,6 @@
{
"name": "portainer",
"version": "1.13.4",
"version": "1.13.5",
"homepage": "https://github.com/portainer/portainer",
"authors": [
"Anthony Lapenna <anthony.lapenna at gmail dot com>"
@ -43,13 +43,13 @@
"lodash": "4.12.0",
"rdash-ui": "1.0.*",
"moment": "~2.14.1",
"xterm.js": "~2.0.1",
"font-awesome": "~4.7.0",
"ng-file-upload": "~12.2.13",
"splitargs": "~0.2.0",
"bootbox.js": "bootbox#^4.4.0",
"angular-multi-select": "~4.0.0",
"toastr": "~2.1.3"
"toastr": "~2.1.3",
"xterm.js": "~2.8.1"
},
"resolutions": {
"angular": "1.5.11"

View File

@ -1,79 +1,49 @@
#!/usr/bin/env bash
ARCHIVE_BUILD_FOLDER="/tmp/portainer-builds"
VERSION=$1
if [[ $# -ne 1 ]] ; then
echo "Usage: $(basename $0) <VERSION>"
exit 1
fi
# parameters platform, architecture
# parameter: "platform-architecture"
function build_and_push_images() {
PLATFORM=$1
ARCH=$2
docker build -t portainer/portainer:${PLATFORM}-${ARCH}-${VERSION} -f build/linux/Dockerfile .
docker push portainer/portainer:${PLATFORM}-${ARCH}-${VERSION}
docker build -t portainer/portainer:${PLATFORM}-${ARCH} -f build/linux/Dockerfile .
docker push portainer/portainer:${PLATFORM}-${ARCH}
docker build -t "portainer/portainer:$1-${VERSION}" -f build/linux/Dockerfile .
docker tag "portainer/portainer:$1-${VERSION}" "portainer/portainer:$1"
docker push "portainer/portainer:$1-${VERSION}"
docker push "portainer/portainer:$1"
}
# parameters: platform, architecture
# parameter: "platform-architecture"
function build_archive() {
PLATFORM=$1
ARCH=$2
BUILD_FOLDER=${ARCHIVE_BUILD_FOLDER}/${PLATFORM}-${ARCH}
BUILD_FOLDER="${ARCHIVE_BUILD_FOLDER}/$1"
rm -rf ${BUILD_FOLDER} && mkdir -pv ${BUILD_FOLDER}/portainer
mv dist/* ${BUILD_FOLDER}/portainer/
cd ${BUILD_FOLDER}
tar cvpfz portainer-${VERSION}-${PLATFORM}-${ARCH}.tar.gz portainer
mv portainer-${VERSION}-${PLATFORM}-${ARCH}.tar.gz ${ARCHIVE_BUILD_FOLDER}/
tar cvpfz "portainer-${VERSION}-$1.tar.gz" portainer
mv "portainer-${VERSION}-$1.tar.gz" ${ARCHIVE_BUILD_FOLDER}/
cd -
}
mkdir -pv /tmp/portainer-builds
function build_all() {
mkdir -pv "${ARCHIVE_BUILD_FOLDER}"
for tag in $@; do
grunt "release:`echo "$tag" | tr '-' ':'`"
name="portainer"; if [ "$(echo "$tag" | cut -c1)" = "w" ]; then name="${name}.exe"; fi
mv dist/portainer-$tag* dist/$name
if [ `echo $tag | cut -d \- -f 1` == 'linux' ]; then build_and_push_images "$tag"; fi
build_archive "$tag"
done
docker rmi $(docker images -q -f dangling=true)
}
PLATFORM="linux"
ARCH="amd64"
grunt release-${PLATFORM}-${ARCH}
build_and_push_images ${PLATFORM} ${ARCH}
build_archive ${PLATFORM} ${ARCH}
if [[ $# -ne 1 ]] ; then
echo "Usage: $(basename $0) <VERSION>"
echo " $(basename $0) \"echo 'Custom' && <BASH COMMANDS>\""
exit 1
else
VERSION="$1"
if [ `echo "$@" | cut -c1-4` == 'echo' ]; then
bash -c "$@";
else
build_all 'linux-amd64 linux-386 linux-arm linux-arm64 linux-ppc64le darwin-amd64 windows-amd64'
exit 0
fi
fi
PLATFORM="linux"
ARCH="386"
grunt release-${PLATFORM}-${ARCH}
build_and_push_images ${PLATFORM} ${ARCH}
build_archive ${PLATFORM} ${ARCH}
PLATFORM="linux"
ARCH="arm"
grunt release-${PLATFORM}-${ARCH}
build_and_push_images ${PLATFORM} ${ARCH}
build_archive ${PLATFORM} ${ARCH}
PLATFORM="linux"
ARCH="arm64"
grunt release-${PLATFORM}-${ARCH}
build_and_push_images ${PLATFORM} ${ARCH}
build_archive ${PLATFORM} ${ARCH}
PLATFORM="linux"
ARCH="ppc64le"
grunt release-${PLATFORM}-${ARCH}
build_and_push_images ${PLATFORM} ${ARCH}
build_archive ${PLATFORM} ${ARCH}
PLATFORM="darwin"
ARCH="amd64"
grunt release-${PLATFORM}-${ARCH}
build_archive ${PLATFORM} ${ARCH}
PLATFORM="windows"
ARCH="amd64"
grunt release-${PLATFORM}-${ARCH}
build_archive ${PLATFORM} ${ARCH}
exit 0

10
build/build_in_container.sh Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
binary="portainer-$1-$2"
mkdir -p dist
docker run --rm -tv $(pwd)/api:/src -e BUILD_GOOS="$1" -e BUILD_GOARCH="$2" portainer/golang-builder:cross-platform /src/cmd/portainer
mv "api/cmd/portainer/$binary" dist/
#sha256sum "dist/$binary" > portainer-checksum.txt

View File

@ -1,33 +1,23 @@
var autoprefixer = require('autoprefixer');
var cssnano = require('cssnano');
var loadGruntTasks = require('load-grunt-tasks');
module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('gruntify-eslint');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-recess');
grunt.loadNpmTasks('grunt-html2js');
grunt.loadNpmTasks('grunt-shell');
grunt.loadNpmTasks('grunt-if');
grunt.loadNpmTasks('grunt-filerev');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-usemin');
grunt.loadNpmTasks('grunt-replace');
grunt.loadNpmTasks('grunt-config');
loadGruntTasks(grunt);
grunt.registerTask('default', ['eslint', 'build']);
grunt.registerTask('build', [
'config:dev',
'clean:app',
'if:linuxAmd64BinaryNotExist',
grunt.registerTask('before-copy', [
'vendor:',
'html2js',
'useminPrepare:dev',
'recess:build',
'useminPrepare:release',
'concat',
'postcss:build',
'clean:tmpl',
'replace',
'copy',
'uglify'
]);
grunt.registerTask('after-copy', [
'filerev',
'usemin',
'clean:tmp'
@ -35,201 +25,48 @@ module.exports = function (grunt) {
grunt.registerTask('build-webapp', [
'config:prod',
'clean:all',
'html2js',
'useminPrepare:release',
'recess:build',
'concat',
'clean:tmpl',
'cssmin',
'replace',
'uglify',
'before-copy',
'copy:assets',
'filerev',
'usemin',
'clean:tmp'
'after-copy'
]);
grunt.registerTask('release-linux-386', [
'config:prod',
'clean:all',
'if:linux386BinaryNotExist',
grunt.registerTask('build', [
'config:dev',
'clean:app',
'shell:buildBinary:linux:amd64',
'vendor:regular',
'html2js',
'useminPrepare:release',
'recess:build',
'useminPrepare:dev',
'concat',
'clean:tmpl',
'cssmin',
'replace',
'uglify',
'copy:assets',
'filerev',
'usemin',
'clean:tmp'
]);
grunt.registerTask('release-linux-amd64', [
'config:prod',
'clean:all',
'if:linuxAmd64BinaryNotExist',
'html2js',
'useminPrepare:release',
'recess:build',
'concat',
'clean:tmpl',
'cssmin',
'replace',
'uglify',
'copy:assets',
'filerev',
'usemin',
'clean:tmp'
]);
grunt.registerTask('release-linux-arm', [
'config:prod',
'clean:all',
'if:linuxArmBinaryNotExist',
'html2js',
'useminPrepare:release',
'recess:build',
'concat',
'clean:tmpl',
'cssmin',
'replace',
'uglify',
'copy',
'filerev',
'usemin',
'clean:tmp'
'after-copy'
]);
grunt.registerTask('release-linux-arm64', [
'config:prod',
'clean:all',
'if:linuxArm64BinaryNotExist',
'html2js',
'useminPrepare:release',
'recess:build',
'concat',
'clean:tmpl',
'cssmin',
'replace',
'uglify',
'copy',
'filerev',
'usemin',
'clean:tmp'
]);
grunt.registerTask('release-linux-ppc64le', [
'config:prod',
'clean:all',
'if:linuxPpc64leBinaryNotExist',
'html2js',
'useminPrepare:release',
'recess:build',
'concat',
'clean:tmpl',
'cssmin',
'replace',
'uglify',
'copy',
'filerev',
'usemin',
'clean:tmp'
]);
grunt.registerTask('release-windows-amd64', [
'config:prod',
'clean:all',
'if:windowsAmd64BinaryNotExist',
'html2js',
'useminPrepare:release',
'recess:build',
'concat',
'clean:tmpl',
'cssmin',
'replace',
'uglify',
'copy',
'filerev',
'usemin',
'clean:tmp'
]);
grunt.registerTask('release-darwin-amd64', [
'config:prod',
'clean:all',
'if:darwinAmd64BinaryNotExist',
'html2js',
'useminPrepare:release',
'recess:build',
'concat',
'clean:tmpl',
'cssmin',
'replace',
'uglify',
'copy',
'filerev',
'usemin',
'clean:tmp'
]);
grunt.registerTask('lint', ['eslint']);
grunt.registerTask('run', ['if:linuxAmd64BinaryNotExist', 'build', 'shell:buildImage', 'shell:run']);
grunt.registerTask('run-dev', ['if:linuxAmd64BinaryNotExist', 'shell:buildImage', 'shell:run', 'watch:build']);
grunt.registerTask('clear', ['clean:app']);
// Print a timestamp (useful for when watching)
grunt.registerTask('timestamp', function () {
grunt.log.subhead(Date());
grunt.task.registerTask('release', 'release:<platform>:<arch>', function(p, a) {
grunt.task.run(['config:prod', 'clean:all', 'shell:buildBinary:'+p+':'+a, 'before-copy', 'copy:assets', 'after-copy' ]);
});
grunt.registerTask('lint', ['eslint']);
grunt.registerTask('run-dev', ['build', 'shell:run', 'watch:build']);
grunt.registerTask('clear', ['clean:app']);
// Project configuration.
grunt.initConfig({
distdir: 'dist',
pkg: grunt.file.readJSON('package.json'),
config: {
dev: {
options: {
variables: {
'environment': 'development'
}
}
},
prod: {
options: {
variables: {
'environment': 'production'
}
}
}
dev: { options: { variables: { 'environment': 'development' }}},
prod: { options: { variables: { 'environment': 'production' }}}
},
src: {
js: ['app/**/*.js', '!app/**/*.spec.js'],
jsTpl: ['<%= distdir %>/templates/**/*.js'],
jsVendor: [
'bower_components/jquery/dist/jquery.min.js',
'bower_components/bootstrap/dist/js/bootstrap.min.js',
'bower_components/Chart.js/Chart.min.js',
'bower_components/lodash/dist/lodash.min.js',
'bower_components/splitargs/src/splitargs.js',
'bower_components/filesize/lib/filesize.min.js',
'bower_components/moment/min/moment.min.js',
'bower_components/xterm.js/dist/xterm.js',
'bower_components/bootbox.js/bootbox.js',
'bower_components/angular-multi-select/isteven-multi-select.js',
'bower_components/toastr/toastr.min.js',
'assets/js/legend.js' // Not a bower package
],
html: ['index.html'],
tpl: ['app/components/**/*.html', 'app/directives/**/*.html'],
css: ['assets/css/app.css'],
cssVendor: [
'bower_components/bootstrap/dist/css/bootstrap.css',
'bower_components/font-awesome/css/font-awesome.min.css',
'bower_components/rdash-ui/dist/css/rdash.min.css',
'bower_components/angular-ui-select/dist/select.min.css',
'bower_components/xterm.js/dist/xterm.css',
'bower_components/angular-multi-select/isteven-multi-select.css',
'bower_components/toastr/toastr.min.css'
]
css: ['assets/css/app.css']
},
clean: {
all: ['<%= distdir %>/*'],
app: ['<%= distdir %>/*', '!<%= distdir %>/portainer'],
app: ['<%= distdir %>/*', '!<%= distdir %>/portainer*'],
tmpl: ['<%= distdir %>/templates'],
tmp: ['<%= distdir %>/js/*', '!<%= distdir %>/js/app.*.js', '<%= distdir %>/css/*', '!<%= distdir %>/css/app.*.css']
},
@ -253,277 +90,98 @@ module.exports = function (grunt) {
}
}
},
filerev: {
files: {
src: ['<%= distdir %>/js/*.js', '<%= distdir %>/css/*.css']
}
},
usemin: {
html: ['<%= distdir %>/index.html']
},
filerev: { files: { src: ['<%= distdir %>/js/*.js', '<%= distdir %>/css/*.css'] }},
usemin: { html: ['<%= distdir %>/index.html'] },
copy: {
bundle: {
files: [
{
dest: '<%= distdir %>/js/',
src: ['app.js'],
expand: true,
cwd: '.tmp/concat/js/'
},
{
dest: '<%= distdir %>/css/',
src: ['app.css'],
expand: true,
cwd: '.tmp/concat/css/'
}
{dest:'<%= distdir %>/js/', src: ['app.js'], expand: true, cwd: '.tmp/concat/js/' },
{dest:'<%= distdir %>/css/', src: ['app.css'], expand: true, cwd: '.tmp/concat/css/' }
]
},
assets: {
files: [
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/bootstrap/fonts/'},
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/font-awesome/fonts/'},
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/rdash-ui/dist/fonts/'},
{
dest: '<%= distdir %>/images/',
src: ['**'],
expand: true,
cwd: 'assets/images/'
},
{dest: '<%= distdir %>/ico', src: '**', expand: true, cwd: 'assets/ico'}
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/bootstrap/fonts/'},
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/font-awesome/fonts/'},
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/rdash-ui/dist/fonts/'},
{dest: '<%= distdir %>/images/', src: '**', expand: true, cwd: 'assets/images/'},
{dest: '<%= distdir %>/ico', src: '**', expand: true, cwd: 'assets/ico'}
]
}
},
eslint: {
src: ['gruntfile.js', '<%= src.js %>'],
options: { configFile: '.eslintrc.yml' }
},
html2js: {
app: {
options: {
base: '.'
},
options: { base: '.' },
src: ['<%= src.tpl %>'],
dest: '<%= distdir %>/templates/app.js',
module: '<%= pkg.name %>.templates'
}
},
concat: {
dist: {
options: {
process: true
},
src: ['<%= src.js %>', '<%= src.jsTpl %>'],
dest: '<%= distdir %>/js/<%= pkg.name %>.js'
},
vendor: {
src: ['<%= src.jsVendor %>'],
dest: '<%= distdir %>/js/vendor.js'
},
index: {
src: ['index.html'],
dest: '<%= distdir %>/index.html',
options: {
process: true
files: {
'<%= distdir %>/css/<%= pkg.name %>.css': ['<%= src.cssVendor %>', '<%= src.css %>'],
'<%= distdir %>/js/vendor.js': ['<%= src.jsVendor %>'],
'<%= distdir %>/js/angular.js': ['<%= src.angularVendor %>']
}
},
angular: {
src: ['bower_components/angular/angular.min.js',
'bower_components/angular-sanitize/angular-sanitize.min.js',
'bower_components/angular-cookies/angular-cookies.min.js',
'bower_components/angular-local-storage/dist/angular-local-storage.min.js',
'bower_components/angular-jwt/dist/angular-jwt.min.js',
'bower_components/angular-ui-router/release/angular-ui-router.min.js',
'bower_components/angular-resource/angular-resource.min.js',
'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
'bower_components/ng-file-upload/ng-file-upload.min.js',
'bower_components/angular-utils-pagination/dirPagination.js',
'bower_components/angular-google-analytics/dist/angular-google-analytics.min.js',
'bower_components/angular-ui-select/dist/select.min.js'],
dest: '<%= distdir %>/js/angular.js'
dist: {
options: { process: true },
files: {
'<%= distdir %>/js/<%= pkg.name %>.js': ['<%= src.js %>', '<%= src.jsTpl %>'],
'<%= distdir %>/index.html': ['index.html']
}
}
},
uglify: {
dist: {
src: ['<%= src.js %>', '<%= src.jsTpl %>'],
dest: '<%= distdir %>/js/<%= pkg.name %>.js'
files: { '<%= distdir %>/js/<%= pkg.name %>.js': ['<%= src.js %>', '<%= src.jsTpl %>'] }
},
vendor: {
options: {
preserveComments: 'some' // Preserve license comments
},
src: ['<%= src.jsVendor %>'],
dest: '<%= distdir %>/js/vendor.js'
},
angular: {
options: {
preserveComments: 'some' // Preserve license comments
},
src: ['<%= concat.angular.src %>'],
dest: '<%= distdir %>/js/angular.js'
options: { preserveComments: 'some' }, // Preserve license comments
files: { '<%= distdir %>/js/vendor.js': ['<%= src.jsVendor %>'] ,
'<%= distdir %>/js/angular.js': ['<%= src.angularVendor %>']
}
}
},
recess: { // TODO: not maintained, unable to preserve license comments, switch out for something better.
postcss: {
build: {
files: {
'<%= distdir %>/css/<%= pkg.name %>.css': ['<%= src.css %>'],
'<%= distdir %>/css/vendor.css': ['<%= src.cssVendor %>']
},
options: {
compile: true,
noOverqualifying: false // TODO: Added because of .nav class, rename
}
},
min: {
files: {
'<%= distdir %>/css/<%= pkg.name %>.css': ['<%= src.css %>'],
'<%= distdir %>/css/vendor.css': ['<%= src.cssVendor %>']
processors: [
autoprefixer({browsers: 'last 2 versions'}), // add vendor prefixes
cssnano() // minify the result
]
},
options: {
compile: true,
compress: true,
noOverqualifying: false // TODO: Added because of .nav class, rename
}
src: '<%= distdir %>/css/<%= pkg.name %>.css',
dest: '<%= distdir %>/css/app.css'
}
},
watch: {
all: {
files: ['<%= src.js %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
tasks: ['default', 'timestamp']
},
build: {
files: ['<%= src.js %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
tasks: ['build', 'shell:buildImage', 'shell:run', 'shell:cleanImages']
/*
* Why don't we just use a host volume
* http.FileServer uses sendFile which virtualbox hates
* Tried using a host volume with -v, copying files with `docker cp`, restating container, none worked
* Rebuilding image on each change was only method that worked, takes ~4s per change to update
*/
},
buildSwarm: {
files: ['<%= src.js %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
tasks: ['build', 'shell:buildImage', 'shell:runSwarm', 'shell:cleanImages']
},
buildSsl: {
files: ['<%= src.js %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
tasks: ['build', 'shell:buildImage', 'shell:runSsl', 'shell:cleanImages']
tasks: ['build']
}
},
eslint: {
src: ['gruntfile.js', '<%= src.js %>'],
options: {
configFile: '.eslintrc.yml'
}
},
shell: {
buildImage: {
command: 'docker build --rm -t portainer -f build/linux/Dockerfile .'
},
buildLinuxAmd64Binary: {
command: [
'docker run --rm -v $(pwd)/api:/src portainer/golang-builder /src/cmd/portainer',
'shasum api/cmd/portainer/portainer > portainer-checksum.txt',
'mkdir -p dist',
'mv api/cmd/portainer/portainer dist/'
].join(' && ')
},
buildLinux386Binary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="386" portainer/golang-builder:cross-platform /src/cmd/portainer',
'shasum api/cmd/portainer/portainer-linux-386 > portainer-checksum.txt',
'mkdir -p dist',
'mv api/cmd/portainer/portainer-linux-386 dist/portainer'
].join(' && ')
},
buildLinuxArmBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" portainer/golang-builder:cross-platform /src/cmd/portainer',
'shasum api/cmd/portainer/portainer-linux-arm > portainer-checksum.txt',
'mkdir -p dist',
'mv api/cmd/portainer/portainer-linux-arm dist/portainer'
].join(' && ')
},
buildLinuxArm64Binary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm64" portainer/golang-builder:cross-platform /src/cmd/portainer',
'shasum api/cmd/portainer/portainer-linux-arm64 > portainer-checksum.txt',
'mkdir -p dist',
'mv api/cmd/portainer/portainer-linux-arm64 dist/portainer'
].join(' && ')
},
buildLinuxPpc64leBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="ppc64le" portainer/golang-builder:cross-platform /src/cmd/portainer',
'shasum api/cmd/portainer/portainer-linux-ppc64le > portainer-checksum.txt',
'mkdir -p dist',
'mv api/cmd/portainer/portainer-linux-ppc64le dist/portainer'
].join(' && ')
},
buildDarwinAmd64Binary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform /src/cmd/portainer',
'shasum api/cmd/portainer/portainer-darwin-amd64 > portainer-checksum.txt',
'mkdir -p dist',
'mv api/cmd/portainer/portainer-darwin-amd64 dist/portainer'
].join(' && ')
},
buildWindowsAmd64Binary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" portainer/golang-builder:cross-platform /src/cmd/portainer',
'shasum api/cmd/portainer/portainer-windows-amd64 > portainer-checksum.txt',
'mkdir -p dist',
'mv api/cmd/portainer/portainer-windows-amd64 dist/portainer.exe'
].join(' && ')
buildBinary: {
command: function (p, a) {
var binfile = 'dist/portainer-'+p+'-'+a;
if (grunt.file.isFile( ( p === 'windows' ) ? binfile+'.exe' : binfile )) {
return 'echo \'BinaryExists\'';
} else {
return 'build/build_in_container.sh ' + p + ' ' + a;
}
}
},
run: {
command: [
'docker stop portainer',
'docker rm portainer',
'docker run --privileged -d -p 9000:9000 -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer --no-analytics'
'docker rm -f portainer',
'docker run -d -p 9000:9000 -v $(pwd)/dist:/app -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer centurylink/ca-certs /app/portainer-linux-amd64 --no-analytics -a /app'
].join(';')
},
cleanImages: {
command: 'docker rmi $(docker images -q -f dangling=true)'
}
},
'if': {
linuxAmd64BinaryNotExist: {
options: {
executable: 'dist/portainer'
},
ifFalse: ['shell:buildLinuxAmd64Binary']
},
linux386BinaryNotExist: {
options: {
executable: 'dist/portainer'
},
ifFalse: ['shell:buildLinux386Binary']
},
linuxArmBinaryNotExist: {
options: {
executable: 'dist/portainer'
},
ifFalse: ['shell:buildLinuxArmBinary']
},
linuxArm64BinaryNotExist: {
options: {
executable: 'dist/portainer'
},
ifFalse: ['shell:buildLinuxArm64Binary']
},
linuxPpc64leBinaryNotExist: {
options: {
executable: 'dist/portainer'
},
ifFalse: ['shell:buildLinuxPpc64leBinary']
},
darwinAmd64BinaryNotExist: {
options: {
executable: 'dist/portainer'
},
ifFalse: ['shell:buildDarwinAmd64Binary']
},
windowsAmd64BinaryNotExist: {
options: {
executable: 'dist/portainer.exe'
},
ifFalse: ['shell:buildWindowsAmd64Binary']
}
},
replace: {
@ -551,4 +209,14 @@ module.exports = function (grunt) {
}
}
});
grunt.registerTask('vendor', 'vendor:<min|reg>', function(min) {
// The content of `vendor.yml` is loaded to src.jsVendor, src.cssVendor and src.angularVendor
// Argument `min` selects between the 'regular' or 'minified' sets
var m = ( min === '' ) ? 'minified' : min;
var v = grunt.file.readYAML('vendor.yml');
for (type in v) { if (v.hasOwnProperty(type)) {
grunt.config('src.'+type+'Vendor',v[type][m]);
}}
});
};

View File

@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "1.13.4",
"version": "1.13.5",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"
@ -22,9 +22,10 @@
"engines": {
"node": ">= 0.8.4"
},
"dependencies": {},
"devDependencies": {
"autoprefixer": "^7.1.1",
"bower": "^1.5.2",
"cssnano": "^3.10.0",
"eslint": "^3.19.0",
"grunt": "~0.4.0",
"grunt-cli": "^1.2.0",
@ -32,19 +33,18 @@
"grunt-contrib-clean": "~0.4.0",
"grunt-contrib-concat": "~0.1.3",
"grunt-contrib-copy": "~0.4.0",
"grunt-contrib-cssmin": "^1.0.2",
"grunt-contrib-jshint": "^1.1.0",
"grunt-contrib-uglify": "^0.9.2",
"grunt-contrib-watch": "~0.3.1",
"grunt-filerev": "^2.3.1",
"grunt-html2js": "~0.1.0",
"grunt-if": "^0.1.5",
"grunt-karma": "~0.4.4",
"grunt-recess": "~0.3",
"grunt-postcss": "^0.8.0",
"grunt-replace": "^1.0.1",
"grunt-shell": "^1.1.2",
"grunt-usemin": "^3.1.1",
"gruntify-eslint": "^3.1.0"
"gruntify-eslint": "^3.1.0",
"load-grunt-tasks": "^3.5.2"
},
"scripts": {
"postinstall": "bower install"

Some files were not shown because too many files have changed in this diff Show More