mirror of https://github.com/portainer/portainer
commit
a438357b45
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
18
app/app.js
18
app/app.js
|
@ -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: {
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
}]);
|
|
@ -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();
|
||||
}]);
|
|
@ -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> > <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>
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > 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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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});
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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> > 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">
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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> > 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>
|
||||
|
|
|
@ -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> > Add registry
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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> > 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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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> > 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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> > 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">
|
||||
|
|
|
@ -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> > <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
|
|
@ -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> > <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a> > Access management
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -69,6 +69,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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> > <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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
|
|
|
@ -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> > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> > <a ui-sref="node({id: node.Id})">{{ node.Hostname }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> > <a ui-sref="registry({id: registry.Id})">{{ registry.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
|
|
@ -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> > <a ui-sref="registry({id: registry.Id})">{{ registry.Name }}</a> > Access management
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
|
|
@ -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> > <a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
|
|
@ -63,6 +63,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> > <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 & published ports</a></li>
|
||||
<li><a href ng-click="goToItem('service-resources')">Resource limits & 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>
|
||||
|
|
|
@ -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();
|
||||
}]);
|
||||
|
|
|
@ -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();
|
||||
|
||||
}]);
|
|
@ -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>
|
|
@ -128,6 +128,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
|
|
@ -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> > <a ui-sref="service({id: service.Id })">{{ service.Name }}</a> > {{ task.Id }}
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
|
|
@ -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> > <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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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> > <a ui-sref="user({id: user.Id})">{{ user.Username }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> > <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">
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -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>
|
|
@ -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();
|
||||
}]);
|
|
@ -0,0 +1,6 @@
|
|||
function AccessControlFormData() {
|
||||
this.AccessControlEnabled = true;
|
||||
this.Ownership = 'private';
|
||||
this.AuthorizedUsers = [];
|
||||
this.AuthorizedTeams = [];
|
||||
}
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -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>
|
|
@ -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();
|
||||
}]);
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ');
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 [];
|
||||
}
|
||||
|
||||
};
|
||||
}]);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
|
@ -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' } }
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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;
|
||||
}]);
|
|
@ -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;
|
||||
}]);
|
||||
|
|
|
@ -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;
|
||||
}]);
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -40,6 +40,10 @@ angular.module('portainer.services')
|
|||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.dataUsage = function () {
|
||||
return System.dataUsage().$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 '';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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"
|
||||
|
|
96
build.sh
96
build.sh
|
@ -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
|
||||
|
|
|
@ -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
|
508
gruntfile.js
508
gruntfile.js
|
@ -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]);
|
||||
}}
|
||||
});
|
||||
};
|
||||
|
|
12
package.json
12
package.json
|
@ -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
Loading…
Reference in New Issue