1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-07 23:05:26 +02:00

refactor(app): introduce new project structure for the frontend (#1623)

This commit is contained in:
Anthony Lapenna 2018-02-01 13:27:52 +01:00 committed by GitHub
parent e6422a6d75
commit 27dceadba1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
354 changed files with 1518 additions and 1755 deletions

266
app/portainer/__module.js Normal file
View file

@ -0,0 +1,266 @@
angular.module('portainer.app', [])
.config(['$stateRegistryProvider', function ($stateRegistryProvider) {
'use strict';
var root = {
name: 'root',
abstract: true,
resolve: {
requiresLogin: ['StateManager', function (StateManager) {
var applicationState = StateManager.getState();
return applicationState.application.authentication;
}]
},
views: {
'sidebar@': {
templateUrl: 'app/portainer/views/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
};
var portainer = {
name: 'portainer',
parent: 'root',
abstract: true
};
var about = {
name: 'portainer.about',
url: '/about',
views: {
'content@': {
templateUrl: 'app/portainer/views/about/about.html'
}
}
};
var account = {
name: 'portainer.account',
url: '/account',
views: {
'content@': {
templateUrl: 'app/portainer/views/account/account.html',
controller: 'AccountController'
}
}
};
var authentication = {
name: 'portainer.auth',
url: '/auth',
params: {
logout: false,
error: ''
},
views: {
'content@': {
templateUrl: 'app/portainer/views/auth/auth.html',
controller: 'AuthenticationController'
},
'sidebar@': {}
},
data: {
requiresLogin: false
}
};
var init = {
name: 'portainer.init',
abstract: true,
url: '/init',
data: {
requiresLogin: false
},
views: {
'sidebar@': {}
}
};
var initEndpoint = {
name: 'portainer.init.endpoint',
url: '/endpoint',
views: {
'content@': {
templateUrl: 'app/portainer/views/init/endpoint/initEndpoint.html',
controller: 'InitEndpointController'
}
}
};
var initAdmin = {
name: 'portainer.init.admin',
url: '/admin',
views: {
'content@': {
templateUrl: 'app/portainer/views/init/admin/initAdmin.html',
controller: 'InitAdminController'
}
}
};
var endpoints = {
name: 'portainer.endpoints',
url: '/endpoints',
views: {
'content@': {
templateUrl: 'app/portainer/views/endpoints/endpoints.html',
controller: 'EndpointsController'
}
}
};
var endpoint = {
name: 'portainer.endpoints.endpoint',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/portainer/views/endpoints/edit/endpoint.html',
controller: 'EndpointController'
}
}
};
var endpointAccess = {
name: 'portainer.endpoints.endpoint.access',
url: '/access',
views: {
'content@': {
templateUrl: 'app/portainer/views/endpoints/access/endpointAccess.html',
controller: 'EndpointAccessController'
}
}
};
var registries = {
name: 'portainer.registries',
url: '/registries',
views: {
'content@': {
templateUrl: 'app/portainer/views/registries/registries.html',
controller: 'RegistriesController'
}
}
};
var registry = {
name: 'portainer.registries.registry',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/portainer/views/registries/edit/registry.html',
controller: 'RegistryController'
}
}
};
var registryCreation = {
name: 'portainer.registries.new',
url: '/new',
views: {
'content@': {
templateUrl: 'app/portainer/views/registries/create/createregistry.html',
controller: 'CreateRegistryController'
}
}
};
var registryAccess = {
name: 'portainer.registries.registry.access',
url: '/access',
views: {
'content@': {
templateUrl: 'app/portainer/views/registries/access/registryAccess.html',
controller: 'RegistryAccessController'
}
}
};
var settings = {
name: 'portainer.settings',
url: '/settings',
views: {
'content@': {
templateUrl: 'app/portainer/views/settings/settings.html',
controller: 'SettingsController'
}
}
};
var settingsAuthentication = {
name: 'portainer.settings.authentication',
url: '/auth',
views: {
'content@': {
templateUrl: 'app/portainer/views/settings/authentication/settingsAuthentication.html',
controller: 'SettingsAuthenticationController'
}
}
};
var users = {
name: 'portainer.users',
url: '/users',
views: {
'content@': {
templateUrl: 'app/portainer/views/users/users.html',
controller: 'UsersController'
}
}
};
var user = {
name: 'portainer.users.user',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/portainer/views/users/edit/user.html',
controller: 'UserController'
}
}
};
var teams = {
name: 'portainer.teams',
url: '/teams',
views: {
'content@': {
templateUrl: 'app/portainer/views/teams/teams.html',
controller: 'TeamsController'
}
}
};
var team = {
name: 'portainer.teams.team',
url: '/:id',
views: {
'content@': {
templateUrl: 'app/portainer/views/teams/edit/team.html',
controller: 'TeamController'
}
}
};
$stateRegistryProvider.register(root);
$stateRegistryProvider.register(portainer);
$stateRegistryProvider.register(about);
$stateRegistryProvider.register(account);
$stateRegistryProvider.register(authentication);
$stateRegistryProvider.register(init);
$stateRegistryProvider.register(initEndpoint);
$stateRegistryProvider.register(initAdmin);
$stateRegistryProvider.register(endpoints);
$stateRegistryProvider.register(endpoint);
$stateRegistryProvider.register(endpointAccess);
$stateRegistryProvider.register(registries);
$stateRegistryProvider.register(registry);
$stateRegistryProvider.register(registryAccess);
$stateRegistryProvider.register(registryCreation);
$stateRegistryProvider.register(settings);
$stateRegistryProvider.register(settingsAuthentication);
$stateRegistryProvider.register(users);
$stateRegistryProvider.register(user);
$stateRegistryProvider.register(teams);
$stateRegistryProvider.register(team);
}]);

View file

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

View file

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

View file

@ -0,0 +1,71 @@
angular.module('portainer.app')
.controller('porAccessControlFormController', ['$q', 'UserService', 'TeamService', 'Notifications', 'Authentication', 'ResourceControlService',
function ($q, UserService, TeamService, 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() {
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true: false;
ctrl.isAdmin = isAdmin;
if (isAdmin) {
ctrl.formData.Ownership = 'administrators';
}
$q.all({
availableTeams: TeamService.teams(),
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');
});
}
initComponent();
}]);

View file

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

View file

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

View file

@ -0,0 +1,189 @@
<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">
<table class="table">
<tbody>
<!-- ownership -->
<tr>
<td>Ownership</td>
<td>
<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="$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="$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="docker.services.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="$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="docker.containers.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>
<tr ng-if="$ctrl.resourceControl.Type === 6 && $ctrl.resourceType !== 'stack'">
<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 stack: {{ $ctrl.resourceControl.ResourceId }}
<portainer-tooltip message="Access control applied on a stack is also applied on each resource in the stack." position="bottom" style="margin-left: 2px;"></portainer-tooltip>
</td>
</tr>
<!-- authorized-users -->
<tr ng-if="$ctrl.resourceControl.UserAccesses.length > 0">
<td>Authorized users</td>
<td>
<span ng-repeat="user in $ctrl.authorizedUsers">{{user.Username}}{{$last ? '' : ', '}} </span>
</td>
</tr>
<!-- !authorized-users -->
<!-- authorized-teams -->
<tr ng-if="$ctrl.resourceControl.TeamAccesses.length > 0">
<td>Authorized teams</td>
<td>
<span ng-repeat="team in $ctrl.authorizedTeams">{{team.Name}}{{$last ? '' : ', '}} </span>
</td>
</tr>
<!-- !authorized-teams -->
<!-- edit-ownership -->
<tr ng-if="!($ctrl.resourceControl.Type === 1 && $ctrl.resourceType === 'volume')
&& !($ctrl.resourceControl.Type === 2 && $ctrl.resourceType === 'container')
&& !($ctrl.resourceControl.Type === 6 && $ctrl.resourceType !== 'stack')
&& !$ctrl.state.editOwnership
&& ($ctrl.isAdmin || $ctrl.state.canEditOwnership)">
<td colspan="2">
<a 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="$ctrl.state.editOwnership">
<td colspan="2">
<div class="boxselector_wrapper">
<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>
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.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>
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 && $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="$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>
<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>
Public
</div>
<p>I want any user with access to this endpoint to be able to manage this resource</p>
</label>
</div>
</div>
</td>
</tr>
<!-- edit-ownership-choices -->
<!-- select-teams -->
<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="$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="portainer.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.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...'}">
</span>
</td>
</tr>
<!-- !select-teams -->
<!-- select-users -->
<tr ng-if="$ctrl.isAdmin && $ctrl.state.editOwnership && $ctrl.formValues.Ownership === 'restricted'">
<td colspan="2">
<span>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="portainer.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.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...'}">
</span>
</td>
</tr>
<!-- !select-users -->
<!-- ownership-actions -->
<tr ng-if="$ctrl.state.editOwnership">
<td colspan="2">
<div>
<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>
<!-- !ownership-actions -->
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -0,0 +1,146 @@
angular.module('portainer.app')
.controller('porAccessControlPanelController', ['$q', '$state', 'UserService', 'TeamService', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'FormValidator',
function ($q, $state, UserService, TeamService, 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() {
var resourceId = ctrl.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');
});
}
function initComponent() {
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 ? TeamService.teams() : []
});
})
.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');
});
}
initComponent();
}]);

View file

@ -0,0 +1,8 @@
angular.module('portainer.app').component('porAccessManagement', {
templateUrl: 'app/portainer/components/accessManagement/porAccessManagement.html',
controller: 'porAccessManagementController',
bindings: {
accessControlledEntity: '<',
updateAccess: '&'
}
});

View file

@ -0,0 +1,134 @@
<div class="row">
<div class="col-sm-6">
<rd-widget>
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Users and teams">
<div class="pull-md-right pull-lg-right">
Items per page:
<select ng-model="$ctrl.state.pagination_count_accesses" ng-change="$ctrl.changePaginationCountAccesses()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-sm-12 nopadding">
<div class="col-sm-12 col-md-6 nopadding">
<button class="btn btn-primary btn-sm" ng-click="$ctrl.authorizeAllAccesses()" ng-disabled="$ctrl.accesses.length === 0 || $ctrl.filteredUsers.length === 0"><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Authorize all</button>
</div>
<div class="col-sm-12 col-md-6 nopadding">
<input type="text" id="filter" ng-model="$ctrl.state.filterUsers" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ng-click="$ctrl.orderAccesses('Name')">
Name
<span ng-show="$ctrl.state.sortAccessesBy == 'Name' && !$ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.state.sortAccessesBy == 'Name' && $ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="$ctrl.orderAccesses('Type')">
Type
<span ng-show="$ctrl.state.sortAccessesBy == 'Type' && !$ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.state.sortAccessesBy == 'Type' && $ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-click="$ctrl.authorizeAccess(user)" class="interactive" dir-paginate="user in $ctrl.accesses | filter:$ctrl.state.filterUsers | orderBy:$ctrl.state.sortAccessesBy:$ctrl.state.sortAccessesReverse | itemsPerPage: $ctrl.state.pagination_count_accesses">
<td>{{ user.Name }}</td>
<td>
<i class="fa" ng-class="user.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
{{ user.Type }}
</td>
</tr>
<tr ng-if="!$ctrl.accesses">
<td colspan="2" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.accesses.length === 0 || ($ctrl.accesses | filter:$ctrl.state.filterUsers | orderBy:$ctrl.state.sortAccessesBy:$ctrl.state.sortAccessesReverse | itemsPerPage: $ctrl.state.pagination_count_accesses).length === 0">
<td colspan="2" class="text-center text-muted">No user or team available.</td>
</tr>
</tbody>
</table>
<div ng-if="$ctrl.accesses" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-sm-6">
<rd-widget>
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Authorized users and teams">
<div class="pull-md-right pull-lg-right">
Items per page:
<select ng-model="$ctrl.state.pagination_count_authorizedAccesses" ng-change="$ctrl.changePaginationCountAuthorizedAccesses()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-sm-12 nopadding">
<div class="col-sm-12 col-md-6 nopadding">
<button class="btn btn-primary btn-sm" ng-click="$ctrl.unauthorizeAllAccesses()" ng-disabled="$ctrl.authorizedAccesses.length === 0 || $ctrl.filteredAuthorizedUsers.length === 0"><i class="fa fa-user-times space-right" aria-hidden="true"></i>Deny all</button>
</div>
<div class="col-sm-12 col-md-6 nopadding">
<input type="text" id="filter" ng-model="$ctrl.state.filterAuthorizedUsers" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ng-click="$ctrl.orderAuthorizedAccesses('Name')">
Name
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Name' && !$ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Name' && $ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="$ctrl.orderAuthorizedAccesses('Type')">
Type
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Type' && !$ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Type' && $ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-click="$ctrl.unauthorizeAccess(user)" class="interactive" pagination-id="table_authaccess" dir-paginate="user in $ctrl.authorizedAccesses | filter:$ctrl.state.filterAuthorizedUsers | orderBy:$ctrl.state.sortAuthorizedAccessesBy:$ctrl.state.sortAuthorizedAccessesReverse | itemsPerPage: $ctrl.state.pagination_count_authorizedAccesses">
<td>{{ user.Name }}</td>
<td>
<i class="fa" ng-class="user.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
{{ user.Type }}
</td>
</tr>
<tr ng-if="!$ctrl.authorizedAccesses">
<td colspan="2" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.authorizedAccesses.length === 0 || (authorizedAccesses | filter:state.filterAuthorizedUsers | orderBy:sortTypeAuthorizedAccesses:sortReverseAuthorizedAccesses | itemsPerPage: state.pagination_count_authorizedAccesses).length === 0">
<td colspan="2" class="text-center text-muted">No authorized user or team.</td>
</tr>
</tbody>
</table>
<div ng-if="$ctrl.authorizedAccesses" class="pull-left pagination-controls">
<dir-pagination-controls pagination-id="table_authaccess"></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -0,0 +1,157 @@
angular.module('portainer.app')
.controller('porAccessManagementController', ['AccessService', 'PaginationService', 'Notifications',
function (AccessService, PaginationService, Notifications) {
var ctrl = this;
ctrl.state = {
pagination_count_accesses: PaginationService.getPaginationLimit('access_management_accesses'),
pagination_count_authorizedAccesses: PaginationService.getPaginationLimit('access_management_AuthorizedAccesses'),
sortAccessesBy: 'Type',
sortAccessesReverse: false,
sortAuthorizedAccessesBy: 'Type',
sortAuthorizedAccessesReverse: false
};
ctrl.orderAccesses = function(sortBy) {
ctrl.state.sortAccessesReverse = (ctrl.state.sortAccessesBy === sortBy) ? !ctrl.state.sortAccessesReverse : false;
ctrl.state.sortAccessesBy = sortBy;
};
ctrl.orderAuthorizedAccesses = function(sortBy) {
ctrl.state.sortAuthorizedAccessesReverse = (ctrl.state.sortAuthorizedAccessesBy === sortBy) ? !ctrl.state.sortAuthorizedAccessesReverse : false;
ctrl.state.sortAuthorizedAccessesBy = sortBy;
};
ctrl.changePaginationCountAuthorizedAccesses = function() {
PaginationService.setPaginationLimit('access_management_AuthorizedAccesses', ctrl.state.pagination_count_authorizedAccesses);
};
ctrl.changePaginationCountAccesses = function() {
PaginationService.setPaginationLimit('access_management_accesses', ctrl.state.pagination_count_accesses);
};
function dispatchUserAndTeamIDs(accesses, users, teams) {
angular.forEach(accesses, function (access) {
if (access.Type === 'user') {
users.push(access.Id);
} else if (access.Type === 'team') {
teams.push(access.Id);
}
});
}
function processAuthorizedIDs(accesses, authorizedAccesses) {
var authorizedUserIDs = [];
var authorizedTeamIDs = [];
if (accesses) {
dispatchUserAndTeamIDs(accesses, authorizedUserIDs, authorizedTeamIDs);
}
if (authorizedAccesses) {
dispatchUserAndTeamIDs(authorizedAccesses, authorizedUserIDs, authorizedTeamIDs);
}
return {
userIDs: authorizedUserIDs,
teamIDs: authorizedTeamIDs
};
}
function removeFromAccesses(access, accesses) {
_.remove(accesses, function(n) {
return n.Id === access.Id && n.Type === access.Type;
});
}
function removeFromAccessIDs(accessId, accessIDs) {
_.remove(accessIDs, function(n) {
return n === accessId;
});
}
ctrl.authorizeAccess = function(access) {
var accessData = processAuthorizedIDs(null, ctrl.authorizedAccesses);
var authorizedUserIDs = accessData.userIDs;
var authorizedTeamIDs = accessData.teamIDs;
if (access.Type === 'user') {
authorizedUserIDs.push(access.Id);
} else if (access.Type === 'team') {
authorizedTeamIDs.push(access.Id);
}
ctrl.updateAccess({ userAccesses: authorizedUserIDs, teamAccesses: authorizedTeamIDs })
.then(function success(data) {
removeFromAccesses(access, ctrl.accesses);
ctrl.authorizedAccesses.push(access);
Notifications.success('Accesses successfully updated');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update accesses');
});
};
ctrl.unauthorizeAccess = function(access) {
var accessData = processAuthorizedIDs(null, ctrl.authorizedAccesses);
var authorizedUserIDs = accessData.userIDs;
var authorizedTeamIDs = accessData.teamIDs;
if (access.Type === 'user') {
removeFromAccessIDs(access.Id, authorizedUserIDs);
} else if (access.Type === 'team') {
removeFromAccessIDs(access.Id, authorizedTeamIDs);
}
ctrl.updateAccess({ userAccesses: authorizedUserIDs, teamAccesses: authorizedTeamIDs })
.then(function success(data) {
removeFromAccesses(access, ctrl.authorizedAccesses);
ctrl.accesses.push(access);
Notifications.success('Accesses successfully updated');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update accesses');
});
};
ctrl.unauthorizeAllAccesses = function() {
ctrl.updateAccess({ userAccesses: [], teamAccesses: [] })
.then(function success(data) {
ctrl.accesses = ctrl.accesses.concat(ctrl.authorizedAccesses);
ctrl.authorizedAccesses = [];
Notifications.success('Accesses successfully updated');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update accesses');
});
};
ctrl.authorizeAllAccesses = function() {
var accessData = processAuthorizedIDs(ctrl.accesses, ctrl.authorizedAccesses);
var authorizedUserIDs = accessData.userIDs;
var authorizedTeamIDs = accessData.teamIDs;
ctrl.updateAccess({ userAccesses: authorizedUserIDs, teamAccesses: authorizedTeamIDs })
.then(function success(data) {
ctrl.authorizedAccesses = ctrl.authorizedAccesses.concat(ctrl.accesses);
ctrl.accesses = [];
Notifications.success('Accesses successfully updated');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to update accesses');
});
};
function initComponent() {
var entity = ctrl.accessControlledEntity;
AccessService.accesses(entity.AuthorizedUsers, entity.AuthorizedTeams)
.then(function success(data) {
ctrl.accesses = data.accesses;
ctrl.authorizedAccesses = data.authorizedAccesses;
})
.catch(function error(err) {
ctrl.accesses = [];
ctrl.authorizedAccesses = [];
Notifications.error('Failure', err, 'Unable to retrieve accesses');
});
}
initComponent();
}]);

View file

@ -0,0 +1,13 @@
angular.module('portainer.app')
.directive('autoFocus', ['$timeout', function porAutoFocus($timeout) {
var directive = {
restrict: 'A',
link: function(scope, element) {
$timeout(function() {
element[0].focus();
});
}
};
return directive;
}]);

View file

@ -0,0 +1,13 @@
angular.module('portainer.app')
.directive('buttonSpinner', function buttonSpinner() {
var directive = {
restrict: 'A',
scope: {
spinning: '=buttonSpinner'
},
transclude: true,
template: '<ng-transclude></ng-transclude><span ng-show="spinning"><i class="fa fa-circle-o-notch fa-spin" style="margin-left: 2px;"></i>&nbsp;</span>'
};
return directive;
});

View file

@ -0,0 +1,244 @@
.datatable .toolBar {
background-color: #f6f6f6;
color: #767676;
overflow: auto;
padding: 10px;
}
.datatable .actionBar {
color: #767676;
padding: 10px;
}
.datatable .toolBar .toolBarTitle {
float: left;
margin: 5px 0 0 10px;
}
.datatable .toolBar .settings {
float: right;
margin: 5px 10px 0 0;
}
.datatable .toolBar .setting {
cursor: pointer;
margin-right: 5px;
}
.datatable .toolBar .setting-active {
color: #337ab7;
}
.datatable .searchBar {
border-top: 1px solid #f6f6f6;
padding: 10px;
}
.datatable .searchInput {
background: none;
border: none;
width: 95%;
}
.datatable .searchIcon {
color: #767676;
margin-right: 5px;
}
.datatable .searchInput:active, .datatable .searchInput:focus {
outline: none;
}
.datatable .pagination-controls {
margin-right: 15px;
}
.datatable .table {
margin-bottom: 0;
}
.datatable .footer {
background-color: #f6f6f6;
color: #767676;
overflow: auto;
}
.datatable .footer .infoBar {
float: left;
font-size: 12px;
margin: 15px 0 0 10px;
}
.datatable .footer .paginationControls {
float: right;
margin: 10px 15px 5px 0;
}
.datatable .footer .paginationControls .limitSelector {
font-size: 12px;
margin-right: 15px;
}
.datatable .footer .paginationControls .pagination {
margin: 0;
}
.datatable .pagination > li > a, .pagination > li > span {
float: none;
}
.tableMenu {
color: #767676;
padding: 10px;
}
.tableMenu .menuHeader {
font-size: 16px;
}
.tableMenu .menuContent {
border-bottom: 1px solid #777;
font-size: 12px;
margin: 10px 0;
max-height: 140px;
overflow-y: auto;
}
.tableMenu .menuContent .md-radio:first-child {
margin: 0;
}
.datatable .table-filters thead tr > th {
height: 32px;
vertical-align: middle;
}
.widget .widget-body table thead th .table-filter {
color: #767676;
cursor: pointer;
font-size: 12px !important;
}
.widget .widget-body table thead th .filter-active {
color: #f0ad4e;
font-size: 12px !important;
}
.datatable .filterbar > th {
border: none;
font-size: 12px !important;
padding: 0;
}
.md-checkbox {
margin: 1px 0;
position: relative;
}
.md-checkbox label {
cursor: pointer;
}
.md-checkbox label:before, .md-checkbox label:after {
content: "";
left: 0;
position: absolute;
top: 0;
}
.md-checkbox label:before {
background: #fff;
border: 2px solid black;
border: 2px solid rgba(0, 0, 0, 0.54);
border-radius: 2px;
cursor: pointer;
height: 16px;
transition: background .3s;
width: 16px;
}
.md-checkbox input[type="checkbox"] {
margin-right: 5px;
opacity: 0;
outline: 0;
}
.md-checkbox input[type="checkbox"]:checked + label:before {
background: #337ab7;
border: none;
}
.md-checkbox input[type="checkbox"]:disabled + label:before {
background: #ececec;
border: 2px solid #ddd;
cursor: auto;
}
.md-checkbox input[type="checkbox"]:checked + label:after {
border: 2px solid #fff;
border-right-style: none;
border-top-style: none;
height: 4px;
left: 4px;
top: 5px;
transform: rotate(-45deg);
width: 9px;
}
.md-radio {
margin: 6px 0;
}
.md-radio .md-radio-inline {
display: inline-block;
}
.md-radio input[type="radio"] {
display: none;
}
.md-radio input[type="radio"]:checked + label:before {
animation: ripple 0.2s linear forwards;
border-color: #337ab7;
}
.md-radio input[type="radio"]:checked + label:after {
transform: scale(1);
}
.md-radio label {
cursor: pointer;
display: inline-block;
height: 16px;
margin-bottom: 0;
padding: 0 22px;
position: relative;
vertical-align: bottom;
}
.md-radio label:before, .md-radio label:after {
border-radius: 50%;
content: '';
position: absolute;
transition: all .3s ease;
transition-property: transform, border-color;
}
.md-radio label:before {
border: 2px solid black;
border: 2px solid rgba(0, 0, 0, 0.54);
height: 16px;
left: 0;
top: 0;
width: 16px;
}
.md-radio label:after {
background: #337ab7;
height: 8px;
left: 4px;
top: 4px;
transform: scale(0);
width: 8px;
}

View file

@ -0,0 +1,99 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="actionBar" ng-if="$ctrl.endpointManagement">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<span class="md-checkbox" ng-if="$ctrl.endpointManagement">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('URL')">
URL
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'URL' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'URL' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox" ng-if="$ctrl.endpointManagement">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="portainer.endpoints.endpoint({id: item.Id})" ng-if="$ctrl.endpointManagement">{{ item.Name }}</a>
<span ng-if="!$ctrl.endpointManagement">{{ item.Name }}</span>
</td>
<td>{{ item.URL | stripprotocol }}</td>
<td>
<a ui-sref="portainer.endpoints.endpoint.access({id: item.Id})" ng-if="$ctrl.accessManagement">
<i class="fa fa-users" aria-hidden="true"></i> Manage access
</a>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">No endpoint available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,16 @@
angular.module('portainer.app').component('endpointsDatatable', {
templateUrl: 'app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
endpointManagement: '<',
accessManagement: '<',
removeAction: '<'
}
});

View file

@ -0,0 +1,65 @@
angular.module('portainer.app')
.controller('GenericDatatableController', ['PaginationService', 'DatatableService',
function (PaginationService, DatatableService) {
this.state = {
selectAll: false,
orderBy: this.orderBy,
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
displayTextFilter: false,
selectedItemCount: 0,
selectedItems: []
};
this.changeOrderBy = function(orderField) {
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
this.state.orderBy = orderField;
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
};
this.selectItem = function(item) {
if (item.Checked) {
this.state.selectedItemCount++;
this.state.selectedItems.push(item);
} else {
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
this.state.selectedItemCount--;
}
};
this.selectAll = function() {
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
var item = this.state.filteredDataSet[i];
if (item.Checked !== this.state.selectAll) {
item.Checked = this.state.selectAll;
this.selectItem(item);
}
}
};
this.changePaginationLimit = function() {
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
};
this.updateDisplayTextFilter = function() {
this.state.displayTextFilter = !this.state.displayTextFilter;
if (!this.state.displayTextFilter) {
delete this.state.textFilter;
}
};
this.$onInit = function() {
setDefaults(this);
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
};
function setDefaults(ctrl) {
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
}
}]);

View file

@ -0,0 +1,104 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.registries.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add registry
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('URL')">
URL
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'URL' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'URL' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="portainer.registries.registry({id: item.Id})">{{ item.Name }}</a>
<span ng-if="item.Authentication" style="margin-left: 5px;" class="label label-info image-tag">authentication-enabled</span>
</td>
<td>
{{ item.URL }}
</td>
<td>
<a ui-sref="portainer.registries.registry.access({id: item.Id})" ng-if="$ctrl.accessManagement">
<i class="fa fa-users" aria-hidden="true"></i> Manage access
</a>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">No registry available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,15 @@
angular.module('portainer.app').component('registriesDatatable', {
templateUrl: 'app/portainer/components/datatables/registries-datatable/registriesDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
accessManagement: '<',
removeAction: '<'
}
});

View file

@ -0,0 +1,105 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Image')">
Image
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Image' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Mode')">
Scheduling Mode
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Mode' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Mode' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Ports')">
Published Ports
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Ports' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Ports' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('UpdatedAt')">
Last Update
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'UpdatedAt' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'UpdatedAt' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td><a ui-sref="docker.services.service({id: item.Id})">{{ item.Name }}</a></td>
<td>{{ item.Image | hideshasum }}</td>
<td>
{{ item.Mode }}
<code>{{ item.Tasks | runningtaskscount }}</code> / <code>{{ item.Mode === 'replicated' ? item.Replicas : ($ctrl.nodes | availablenodecount) }}</code>
</td>
<td>
<a ng-if="item.Ports && item.Ports.length > 0 && p.PublishedPort" ng-repeat="p in item.Ports" class="image-tag" ng-href="http://{{ $ctrl.publicUrl }}:{{ p.PublishedPort }}" target="_blank">
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.PublishedPort }}:{{ p.TargetPort }}
</a>
<span ng-if="!item.Ports || item.Ports.length === 0">-</span>
</td>
<td>{{ item.UpdatedAt | getisodate }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="5" class="text-center text-muted">No service available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,15 @@
angular.module('portainer.app').component('stackServicesDatatable', {
templateUrl: 'app/portainer/components/datatables/stack-services-datatable/stackServicesDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
nodes: '<',
publicUrl: '<',
showTextFilter: '<'
}
});

View file

@ -0,0 +1,103 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.stacks.new">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.showOwnershipColumn">
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
Ownership
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.Ownership' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-if="$ctrl.displayExternalStacks || (!$ctrl.displayExternalStacks && !item.External)" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)" ng-disabled="!item.Id"/>
<label for="select_{{ $index }}"></label>
</span>
<a ng-if="item.Id" ui-sref="docker.stacks.stack({ id: item.Id })">{{ item.Name }}</a>
<span ng-if="!item.Id">
{{ item.Name }} <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true"></i>
</span>
</td>
<td ng-if="$ctrl.showOwnershipColumn">
<span>
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="2" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="2" class="text-center text-muted">No stack available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,16 @@
angular.module('portainer.app').component('stacksDatatable', {
templateUrl: 'app/portainer/components/datatables/stacks-datatable/stacksDatatable.html',
controller: 'StacksDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
showOwnershipColumn: '<',
removeAction: '<',
displayExternalStacks: '<'
}
});

View file

@ -0,0 +1,65 @@
angular.module('portainer.app')
.controller('StacksDatatableController', ['PaginationService', 'DatatableService',
function (PaginationService, DatatableService) {
this.state = {
selectAll: false,
orderBy: this.orderBy,
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
displayTextFilter: false,
selectedItemCount: 0,
selectedItems: []
};
this.changeOrderBy = function(orderField) {
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
this.state.orderBy = orderField;
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
};
this.selectItem = function(item) {
if (item.Checked) {
this.state.selectedItemCount++;
this.state.selectedItems.push(item);
} else {
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
this.state.selectedItemCount--;
}
};
this.selectAll = function() {
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
var item = this.state.filteredDataSet[i];
if (item.Id && item.Checked !== this.state.selectAll) {
item.Checked = this.state.selectAll;
this.selectItem(item);
}
}
};
this.changePaginationLimit = function() {
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
};
this.updateDisplayTextFilter = function() {
this.state.displayTextFilter = !this.state.displayTextFilter;
if (!this.state.displayTextFilter) {
delete this.state.textFilter;
}
};
this.$onInit = function() {
setDefaults(this);
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
};
function setDefaults(ctrl) {
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
}
}]);

View file

@ -0,0 +1,84 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="portainer.teams.team({id: item.Id})">{{ item.Name }}</a>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td class="text-center text-muted">No team available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,14 @@
angular.module('portainer.app').component('teamsDatatable', {
templateUrl: 'app/portainer/components/datatables/teams-datatable/teamsDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
removeAction: '<'
}
});

View file

@ -0,0 +1,110 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Username')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Username' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Username' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('RoleName')">
Role
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RoleName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RoleName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('AuthenticationMethod')">
Authentication
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'AuthenticationMethod' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'AuthenticationMethod' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="portainer.users.user({id: item.Id})">{{ item.Username }}</a>
</td>
<td>
<span>
<i class="fa fa-user-circle-o" aria-hidden="true" style="margin-right: 5px;" ng-if="item.Role === 1 && !item.isTeamLeader"></i>
<i class="fa fa-user-plus" aria-hidden="true" style="margin-right: 5px;" ng-if="item.Role !== 1 && item.isTeamLeader"></i>
<i class="fa fa-user" aria-hidden="true" style="margin-right: 5px;" ng-if="item.Role !== 1 && !item.isTeamLeader"></i>
{{ item.RoleName }}
</span>
</td>
<td>
<span ng-if="item.Id === 1 || $ctrl.authenticationMethod !== 2">Internal</span>
<span ng-if="item.Id !== 1 && $ctrl.authenticationMethod === 2">LDAP</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">No user available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,15 @@
angular.module('portainer.app').component('usersDatatable', {
templateUrl: 'app/portainer/components/datatables/users-datatable/usersDatatable.html',
controller: 'GenericDatatableController',
bindings: {
title: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
showTextFilter: '<',
removeAction: '<',
authenticationMethod: '<'
}
});

View file

@ -0,0 +1,12 @@
angular.module('portainer.app').component('porEndpointSecurity', {
templateUrl: 'app/portainer/components/endpointSecurity/porEndpointSecurity.html',
controller: 'porEndpointSecurityController',
bindings: {
// This object will be populated with the form data.
// Model reference in endpointSecurityModel.js
formData: '=',
// The component will use this object to initialize the default values
// if present.
endpoint: '<'
}
});

View file

@ -0,0 +1,126 @@
<div>
<!-- tls-checkbox -->
<div class="form-group">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to connect to the Docker endpoint with TLS."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="$ctrl.formData.TLS"><i></i>
</label>
</div>
</div>
<!-- !tls-checkbox -->
<div class="col-sm-12 form-section-title" ng-if="$ctrl.formData.TLS">
TLS mode
</div>
<!-- note -->
<div class="form-group" ng-if="$ctrl.formData.TLS">
<div class="col-sm-12">
<span class="small text-muted">
You can find out more information about how to protect a Docker environment with TLS in the <a href="https://docs.docker.com/engine/security/https/" target="_blank">Docker documentation</a>.
</span>
</div>
</div>
<div class="form-group"></div>
<!-- endpoint-tls-mode -->
<div class="form-group" style="margin-bottom: 0" ng-if="$ctrl.formData.TLS">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="tls_client_ca" ng-model="$ctrl.formData.TLSMode" value="tls_client_ca">
<label for="tls_client_ca">
<div class="boxselector_header">
<i class="fa fa-shield" aria-hidden="true" style="margin-right: 2px;"></i>
TLS with server and client verification
</div>
<p>Use client certificates and server verification</p>
</label>
</div>
<div>
<input type="radio" id="tls_client_noca" ng-model="$ctrl.formData.TLSMode" value="tls_client_noca">
<label for="tls_client_noca">
<div class="boxselector_header">
<i class="fa fa-shield" aria-hidden="true" style="margin-right: 2px;"></i>
TLS with client verification only
</div>
<p>Use client certificates without server verification</p>
</label>
</div>
<div>
<input type="radio" id="tls_ca" ng-model="$ctrl.formData.TLSMode" value="tls_ca">
<label for="tls_ca">
<div class="boxselector_header">
<i class="fa fa-shield" aria-hidden="true" style="margin-right: 2px;"></i>
TLS with server verification only
</div>
<p>Only verify the server certificate</p>
</label>
</div>
<div>
<input type="radio" id="tls_only" ng-model="$ctrl.formData.TLSMode" value="tls_only">
<label for="tls_only">
<div class="boxselector_header">
<i class="fa fa-shield" aria-hidden="true" style="margin-right: 2px;"></i>
TLS only
</div>
<p>No server/client verification</p>
</label>
</div>
</div>
</div>
<!-- !endpoint-tls-mode -->
<div class="col-sm-12 form-section-title" ng-if="$ctrl.formData.TLS && $ctrl.formData.TLSMode !== 'tls_only'">
Required TLS files
</div>
<!-- tls-file-upload -->
<div ng-if="$ctrl.formData.TLS">
<!-- tls-file-ca -->
<div class="form-group" ng-if="$ctrl.formData.TLSMode === 'tls_client_ca' || $ctrl.formData.TLSMode === 'tls_ca'">
<label class="col-sm-3 col-lg-2 control-label text-left">TLS CA certificate</label>
<div class="col-sm-9 col-lg-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formData.TLSCACert">Select file</button>
<span style="margin-left: 5px;">
{{ $ctrl.formData.TLSCACert.name }}
<i class="fa fa-check green-icon" ng-if="$ctrl.formData.TLSCACert && $ctrl.formData.TLSCACert === $ctrl.endpoint.TLSConfig.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!$ctrl.formData.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !tls-file-ca -->
<!-- tls-files-cert-key -->
<div ng-if="$ctrl.formData.TLSMode === 'tls_client_ca' || $ctrl.formData.TLSMode === 'tls_client_noca'">
<!-- tls-file-cert -->
<div class="form-group">
<label for="tls_cert" class="col-sm-3 col-lg-2 control-label text-left">TLS certificate</label>
<div class="col-sm-9 col-lg-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formData.TLSCert">Select file</button>
<span style="margin-left: 5px;">
{{ $ctrl.formData.TLSCert.name }}
<i class="fa fa-check green-icon" ng-if="$ctrl.formData.TLSCert && $ctrl.formData.TLSCert === $ctrl.endpoint.TLSConfig.TLSCert" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!$ctrl.formData.TLSCert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !tls-file-cert -->
<!-- tls-file-key -->
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">TLS key</label>
<div class="col-sm-9 col-lg-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formData.TLSKey">Select file</button>
<span style="margin-left: 5px;">
{{ $ctrl.formData.TLSKey.name }}
<i class="fa fa-check green-icon" ng-if="$ctrl.formData.TLSKey && $ctrl.formData.TLSKey === $ctrl.endpoint.TLSConfig.TLSKey" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!$ctrl.formData.TLSKey" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !tls-file-key -->
</div>
<!-- tls-files-cert-key -->
</div>
<!-- !tls-file-upload -->
</div>

View file

@ -0,0 +1,32 @@
angular.module('portainer.app')
.controller('porEndpointSecurityController', [function () {
var ctrl = this;
function initComponent() {
if (ctrl.endpoint) {
var endpoint = ctrl.endpoint;
var TLS = endpoint.TLSConfig.TLS;
ctrl.formData.TLS = TLS;
var CACert = endpoint.TLSConfig.TLSCACert;
ctrl.formData.TLSCACert = CACert;
var cert = endpoint.TLSConfig.TLSCert;
ctrl.formData.TLSCert = cert;
var key = endpoint.TLSConfig.TLSKey;
ctrl.formData.TLSKey = key;
if (TLS) {
if (CACert && cert && key) {
ctrl.formData.TLSMode = 'tls_client_ca';
} else if (cert && key) {
ctrl.formData.TLSMode = 'tls_client_noca';
} else if (CACert) {
ctrl.formData.TLSMode = 'tls_ca';
} else {
ctrl.formData.TLSMode = 'tls_only';
}
}
}
}
initComponent();
}]);

View file

@ -0,0 +1,7 @@
function EndpointSecurityFormData() {
this.TLS = false;
this.TLSMode = 'tls_client_ca';
this.TLSCACert = null;
this.TLSCert = null;
this.TLSKey = null;
}

View file

@ -0,0 +1,13 @@
angular.module('portainer.app')
.directive('rdHeaderContent', ['Authentication', function rdHeaderContent(Authentication) {
var directive = {
requires: '^rdHeader',
transclude: true,
link: function (scope, iElement, iAttrs) {
scope.username = Authentication.getUserDetails().username;
},
template: '<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right" ng-if="username"><a ui-sref="portainer.account" style="margin-right: 5px;"><u><i class="fa fa-wrench" aria-hidden="true"></i> my account </u></a><a ui-sref="portainer.auth({logout: true})" class="text-danger" style="margin-right: 25px;"><u><i class="fa fa-sign-out" aria-hidden="true"></i> log out</u></a></div></div>',
restrict: 'E'
};
return directive;
}]);

View file

@ -0,0 +1,17 @@
angular.module('portainer.app')
.directive('rdHeaderTitle', ['Authentication', 'StateManager', function rdHeaderTitle(Authentication, StateManager) {
var directive = {
requires: '^rdHeader',
scope: {
title: '@'
},
link: function (scope, iElement, iAttrs) {
scope.username = Authentication.getUserDetails().username;
scope.displayDonationHeader = StateManager.getState().application.displayDonationHeader;
},
transclude: true,
template: '<div class="page white-space-normal">{{title}}<span class="header_title_content" ng-transclude></span><span class="pull-right user-box" ng-if="username"><i class="fa fa-user-circle-o" aria-hidden="true"></i> {{username}}</span><a ng-if="displayDonationHeader" ui-sref="portainer.about" class="pull-right" style="font-size:14px;margin-right:15px;margin-top:2px;"><span class="fa fa-heart fa-fw red-icon"></span> Help support portainer</a></div>',
restrict: 'E'
};
return directive;
}]);

View file

@ -0,0 +1,12 @@
angular.module('portainer.app')
.directive('rdHeader', function rdHeader() {
var directive = {
scope: {
'ngModel': '='
},
transclude: true,
template: '<div class="row header"><div id="loadingbar-placeholder"></div><div class="col-xs-12"><div class="meta" ng-transclude></div></div></div>',
restrict: 'EA'
};
return directive;
});

View file

@ -0,0 +1,8 @@
angular.module('portainer.app')
.directive('rdLoading', function rdLoading() {
var directive = {
restrict: 'AE',
template: '<div class="loading"><div class="double-bounce1"></div><div class="double-bounce2"></div></div>'
};
return directive;
});

View file

@ -0,0 +1,18 @@
angular.module('portainer.app')
.directive('onEnterKey', [function porOnEnterKey() {
var directive = {
restrict: 'A',
link: function(scope, element, attrs) {
element.bind('keydown keypress', function (e) {
if ( e.which === 13 ) {
e.preventDefault();
scope.$apply(function () {
scope.$eval(attrs.onEnterKey);
});
}
});
}
};
return directive;
}]);

View file

@ -0,0 +1,3 @@
<div>
<rzslider rz-slider-options="$ctrl.options" rz-slider-model="$ctrl.model"></rzslider>
</div>

View file

@ -0,0 +1,12 @@
angular.module('portainer.app').component('slider', {
templateUrl: 'app/portainer/components/slider/slider.html',
controller: 'SliderController',
bindings: {
model: '=',
onChange: '&',
floor: '<',
ceil: '<',
step: '<',
precision: '<'
}
});

View file

@ -0,0 +1,22 @@
angular.module('portainer.app')
.controller('SliderController', function () {
var ctrl = this;
ctrl.options = {
floor: ctrl.floor,
ceil: ctrl.ceil,
step: ctrl.step,
precision: ctrl.precision,
showSelectionBar: true,
enforceStep: false,
translate: function(value, sliderId, label) {
if (label === 'floor' || value === 0) {
return 'unlimited';
}
return value;
},
onChange: function() {
ctrl.onChange();
}
};
});

View file

@ -0,0 +1,12 @@
angular.module('portainer.app')
.directive('rdTemplateWidget', function rdWidget() {
var directive = {
scope: {
'ngModel': '='
},
transclude: true,
template: '<div class="widget template-widget" id="template-widget" ng-transclude></div>',
restrict: 'EA'
};
return directive;
});

View file

@ -0,0 +1,12 @@
angular.module('portainer.app')
.directive('portainerTooltip', [function portainerTooltip() {
var directive = {
scope: {
message: '@',
position: '@'
},
template: '<span class="interactive" tooltip-append-to-body="true" tooltip-placement="{{position}}" tooltip-class="portainer-tooltip" uib-tooltip="{{message}}"><i class="fa fa-question-circle tooltip-icon" aria-hidden="true"></i></span>',
restrict: 'E'
};
return directive;
}]);

View file

@ -0,0 +1,14 @@
angular.module('portainer.app')
.directive('rdWidgetBody', function rdWidgetBody() {
var directive = {
requires: '^rdWidget',
scope: {
loading: '@?',
classes: '@?'
},
transclude: true,
template: '<div class="widget-body" ng-class="classes"><rd-loading ng-show="loading"></rd-loading><div ng-hide="loading" class="widget-content" ng-transclude></div></div>',
restrict: 'E'
};
return directive;
});

View file

@ -0,0 +1,14 @@
angular.module('portainer.app')
.directive('rdWidgetCustomHeader', function rdWidgetCustomHeader() {
var directive = {
requires: '^rdWidget',
scope: {
title: '=',
icon: '='
},
transclude: true,
template: '<div class="widget-header"><div class="row"><span class="pull-left"><img class="custom-header-ico" ng-src="{{icon}}"></img> <span class="text-muted"> {{title}} </span> </span><span class="pull-right col-xs-6 col-sm-4" ng-transclude></span></div></div>',
restrict: 'E'
};
return directive;
});

View file

@ -0,0 +1,10 @@
angular.module('portainer.app')
.directive('rdWidgetFooter', function rdWidgetFooter() {
var directive = {
requires: '^rdWidget',
transclude: true,
template: '<div class="widget-footer" ng-transclude></div>',
restrict: 'E'
};
return directive;
});

View file

@ -0,0 +1,15 @@
angular.module('portainer.app')
.directive('rdWidgetHeader', function rdWidgetTitle() {
var directive = {
requires: '^rdWidget',
scope: {
title: '@',
icon: '@',
classes: '@?'
},
transclude: true,
template: '<div class="widget-header"><div class="row"><span ng-class="classes" class="pull-left"><i class="fa" ng-class="icon"></i> {{title}} </span><span ng-class="classes" class="pull-right" ng-transclude></span></div></div>',
restrict: 'E'
};
return directive;
});

View file

@ -0,0 +1,13 @@
angular.module('portainer.app')
.directive('rdWidgetTaskbar', function rdWidgetTaskbar() {
var directive = {
requires: '^rdWidget',
scope: {
classes: '@?'
},
transclude: true,
template: '<div class="widget-header"><div class="row"><div ng-class="classes" ng-transclude></div></div></div>',
restrict: 'E'
};
return directive;
});

View file

@ -0,0 +1,12 @@
angular.module('portainer.app')
.directive('rdWidget', function rdWidget() {
var directive = {
scope: {
'ngModel': '='
},
transclude: true,
template: '<div class="widget" ng-transclude></div>',
restrict: 'EA'
};
return directive;
});

View file

@ -0,0 +1,119 @@
angular.module('portainer.app')
.filter('truncate', function () {
'use strict';
return function (text, length, end) {
if (isNaN(length)) {
length = 10;
}
if (end === undefined) {
end = '...';
}
if (text.length <= length || text.length - end.length <= length) {
return text;
} 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('capitalize', function () {
'use strict';
return function (text) {
return _.capitalize(text);
};
})
.filter('stripprotocol', function() {
'use strict';
return function (url) {
return url.replace(/.*?:\/\//g, '');
};
})
.filter('humansize', function () {
'use strict';
return function (bytes, round, base) {
if (!round) {
round = 1;
}
if (!base) {
base = 10;
}
if (bytes || bytes === 0) {
return filesize(bytes, {base: base, round: round});
}
};
})
.filter('getisodatefromtimestamp', function () {
'use strict';
return function (timestamp) {
return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss');
};
})
.filter('getisodate', function () {
'use strict';
return function (date) {
return moment(date).format('YYYY-MM-DD HH:mm:ss');
};
})
.filter('key', function () {
'use strict';
return function (pair, separator) {
return pair.slice(0, pair.indexOf(separator));
};
})
.filter('value', function () {
'use strict';
return function (pair, separator) {
return pair.slice(pair.indexOf(separator) + 1);
};
})
.filter('emptyobject', function () {
'use strict';
return function (obj) {
return _.isEmpty(obj);
};
})
.filter('ipaddress', function () {
'use strict';
return function (ip) {
return ip.slice(0, ip.indexOf('/'));
};
})
.filter('arraytostr', function () {
'use strict';
return function (arr, separator) {
if (arr) {
return _.join(arr, separator);
}
return '';
};
})
.filter('ownershipicon', function () {
'use strict';
return function (ownership) {
switch (ownership) {
case 'private':
return 'fa fa-eye-slash';
case 'administrators':
return 'fa fa-eye-slash';
case 'restricted':
return 'fa fa-users';
default:
return 'fa fa-eye';
}
};
});

View file

@ -0,0 +1,18 @@
angular.module('portainer.app')
.factory('FormHelper', [function FormHelperFactory() {
'use strict';
var helper = {};
helper.removeInvalidEnvVars = function(env) {
for (var i = env.length - 1; i >= 0; i--) {
var envvar = env[i];
if (!envvar.value || !envvar.name) {
env.splice(i, 1);
}
}
return env;
};
return helper;
}]);

View file

@ -0,0 +1,18 @@
angular.module('portainer.app')
.factory('RegistryHelper', [function RegistryHelperFactory() {
'use strict';
var helper = {};
helper.getRegistryByURL = function(registries, url) {
for (var i = 0; i < registries.length; i++) {
if (registries[i].URL === url) {
return registries[i];
}
}
return null;
};
return helper;
}]);

View file

@ -0,0 +1,42 @@
angular.module('portainer.app')
.factory('ResourceControlHelper', [function ResourceControlHelperFactory() {
'use strict';
var helper = {};
helper.retrieveAuthorizedUsers = function(resourceControl, users) {
var authorizedUsers = [];
angular.forEach(resourceControl.UserAccesses, function(access) {
var user = _.find(users, { Id: access.UserId });
if (user) {
authorizedUsers.push(user);
}
});
return authorizedUsers;
};
helper.retrieveAuthorizedTeams = function(resourceControl, teams) {
var authorizedTeams = [];
angular.forEach(resourceControl.TeamAccesses, function(access) {
var team = _.find(teams, { Id: access.TeamId });
if (team) {
authorizedTeams.push(team);
}
});
return authorizedTeams;
};
helper.isLeaderOfAnyRestrictedTeams = function(userMemberships, resourceControl) {
var isTeamLeader = false;
for (var i = 0; i < userMemberships.length; i++) {
var membership = userMemberships[i];
var found = _.find(resourceControl.TeamAccesses, { TeamId :membership.TeamId });
if (found && membership.Role === 1) {
isTeamLeader = true;
break;
}
}
return isTeamLeader;
};
return helper;
}]);

View file

@ -0,0 +1,21 @@
angular.module('portainer.app')
.factory('StackHelper', [function StackHelperFactory() {
'use strict';
var helper = {};
helper.getExternalStackNamesFromServices = function(services) {
var stackNames = [];
for (var i = 0; i < services.length; i++) {
var service = services[i];
if (!service.Labels || !service.Labels['com.docker.stack.namespace']) continue;
var stackName = service.Labels['com.docker.stack.namespace'];
stackNames.push(stackName);
}
return _.uniq(stackNames);
};
return helper;
}]);

View file

@ -0,0 +1,138 @@
angular.module('portainer.app')
.factory('TemplateHelper', ['$filter', function TemplateHelperFactory($filter) {
'use strict';
var helper = {};
helper.getDefaultContainerConfiguration = function() {
return {
Env: [],
OpenStdin: false,
Tty: false,
ExposedPorts: {},
HostConfig: {
RestartPolicy: {
Name: 'no'
},
PortBindings: {},
Binds: [],
Privileged: false,
ExtraHosts: []
},
Volumes: {},
Labels: {}
};
};
helper.portArrayToPortConfiguration = function(ports) {
var portConfiguration = {
bindings: {},
exposedPorts: {}
};
ports.forEach(function (p) {
if (p.containerPort) {
var key = p.containerPort + '/' + p.protocol;
var binding = {};
if (p.hostPort) {
binding.HostPort = p.hostPort;
if (p.hostPort.indexOf(':') > -1) {
var hostAndPort = p.hostPort.split(':');
binding.HostIp = hostAndPort[0];
binding.HostPort = hostAndPort[1];
}
}
portConfiguration.bindings[key] = [binding];
portConfiguration.exposedPorts[key] = {};
}
});
return portConfiguration;
};
helper.updateContainerConfigurationWithLabels = function(labelsArray) {
var labels = {};
labelsArray.forEach(function (l) {
if (l.name && l.value) {
labels[l.name] = l.value;
}
});
return labels;
};
helper.EnvToStringArray = function(templateEnvironment, containerMapping) {
var env = [];
templateEnvironment.forEach(function(envvar) {
if (envvar.value || envvar.set) {
var value = envvar.set ? envvar.set : envvar.value;
if (envvar.type && envvar.type === 'container') {
if (containerMapping === 'BY_CONTAINER_IP') {
var container = envvar.value;
value = container.NetworkSettings.Networks[Object.keys(container.NetworkSettings.Networks)[0]].IPAddress;
} else if (containerMapping === 'BY_CONTAINER_NAME') {
value = $filter('containername')(envvar.value);
} else if (containerMapping === 'BY_SWARM_CONTAINER_NAME') {
value = $filter('swarmcontainername')(envvar.value);
}
}
env.push(envvar.name + '=' + value);
}
});
return env;
};
helper.getConsoleConfiguration = function(interactiveFlag) {
var consoleConfiguration = {
openStdin: false,
tty: false
};
if (interactiveFlag === true) {
consoleConfiguration.openStdin = true;
consoleConfiguration.tty = true;
}
return consoleConfiguration;
};
helper.createVolumeBindings = function(volumes, generatedVolumesPile) {
volumes.forEach(function (volume) {
if (volume.containerPath) {
var binding;
if (volume.type === 'auto') {
binding = generatedVolumesPile.pop().Id + ':' + volume.containerPath;
} else if (volume.type !== 'auto' && volume.name) {
binding = volume.name + ':' + volume.containerPath;
}
if (volume.readOnly) {
binding += ':ro';
}
volume.binding = binding;
}
});
};
helper.determineRequiredGeneratedVolumeCount = function(volumes) {
var count = 0;
volumes.forEach(function (volume) {
if (volume.type === 'auto') {
++count;
}
});
return count;
};
helper.filterLinuxServerIOTemplates = function(templates) {
return templates.filter(function f(template) {
var valid = false;
if (template.Categories) {
angular.forEach(template.Categories, function(category) {
if (_.startsWith(category, 'Network')) {
valid = true;
}
});
}
return valid;
}).map(function(template, idx) {
template.index = idx;
return template;
});
};
return helper;
}]);

View file

@ -0,0 +1,15 @@
angular.module('portainer.app')
.factory('UserHelper', [function UserHelperFactory() {
'use strict';
var helper = {};
helper.filterNonAdministratorUsers = function(users) {
return users.filter(function (user) {
if (user.Role !== 1) {
return user;
}
});
};
return helper;
}]);

View file

@ -0,0 +1,11 @@
function UserAccessViewModel(data) {
this.Id = data.Id;
this.Name = data.Username;
this.Type = 'user';
}
function TeamAccessViewModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.Type = 'team';
}

View file

@ -0,0 +1,7 @@
function DockerHubViewModel(data) {
this.Name = 'DockerHub';
this.URL = '';
this.Authentication = data.Authentication;
this.Username = data.Username;
this.Password = data.Password;
}

View file

@ -0,0 +1,11 @@
function RegistryViewModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.URL = data.URL;
this.Authentication = data.Authentication;
this.Username = data.Username;
this.Password = data.Password;
this.AuthorizedUsers = data.AuthorizedUsers;
this.AuthorizedTeams = data.AuthorizedTeams;
this.Checked = false;
}

View file

@ -0,0 +1,19 @@
function ResourceControlViewModel(data) {
this.Id = data.Id;
this.Type = data.Type;
this.ResourceId = data.ResourceId;
this.UserAccesses = data.UserAccesses;
this.TeamAccesses = data.TeamAccesses;
this.AdministratorsOnly = data.AdministratorsOnly;
this.Ownership = determineOwnership(this);
}
function determineOwnership(resourceControl) {
if (resourceControl.AdministratorsOnly) {
return 'administrators';
} else if (resourceControl.UserAccesses.length === 1 && resourceControl.TeamAccesses.length === 0) {
return 'private';
} else if (resourceControl.UserAccesses.length > 1 || resourceControl.TeamAccesses.length > 0) {
return 'restricted';
}
}

View file

@ -0,0 +1,12 @@
function LDAPSettingsViewModel(data) {
this.ReaderDN = data.ReaderDN;
this.Password = data.Password;
this.URL = data.URL;
this.SearchSettings = data.SearchSettings;
}
function LDAPSearchSettings(BaseDN, UsernameAttribute, Filter) {
this.BaseDN = BaseDN;
this.UsernameAttribute = UsernameAttribute;
this.Filter = Filter;
}

View file

@ -0,0 +1,11 @@
function SettingsViewModel(data) {
this.TemplatesURL = data.TemplatesURL;
this.LogoURL = data.LogoURL;
this.BlackListedLabels = data.BlackListedLabels;
this.DisplayDonationHeader = data.DisplayDonationHeader;
this.DisplayExternalContributors = data.DisplayExternalContributors;
this.AuthenticationMethod = data.AuthenticationMethod;
this.LDAPSettings = data.LDAPSettings;
this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers;
this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers;
}

View file

@ -0,0 +1,6 @@
function StatusViewModel(data) {
this.Authentication = data.Authentication;
this.EndpointManagement = data.EndpointManagement;
this.Analytics = data.Analytics;
this.Version = data.Version;
}

View file

@ -0,0 +1,5 @@
function TeamViewModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.Checked = false;
}

View file

@ -0,0 +1,6 @@
function TeamMembershipModel(data) {
this.Id = data.Id;
this.UserId = data.UserID;
this.TeamId = data.TeamID;
this.Role = data.Role;
}

View file

@ -0,0 +1,12 @@
function UserViewModel(data) {
this.Id = data.Id;
this.Username = data.Username;
this.Role = data.Role;
if (data.Role === 1) {
this.RoleName = 'administrator';
} else {
this.RoleName = 'user';
}
this.AuthenticationMethod = data.AuthenticationMethod;
this.Checked = false;
}

View file

@ -0,0 +1,9 @@
angular.module('portainer.app')
.factory('Auth', ['$resource', 'API_ENDPOINT_AUTH', function AuthFactory($resource, API_ENDPOINT_AUTH) {
'use strict';
return $resource(API_ENDPOINT_AUTH, {}, {
login: {
method: 'POST', ignoreLoadingBar: true
}
});
}]);

View file

@ -0,0 +1,8 @@
angular.module('portainer.app')
.factory('DockerHub', ['$resource', 'API_ENDPOINT_DOCKERHUB', function DockerHubFactory($resource, API_ENDPOINT_DOCKERHUB) {
'use strict';
return $resource(API_ENDPOINT_DOCKERHUB, {}, {
get: { method: 'GET' },
update: { method: 'PUT' }
});
}]);

View file

@ -0,0 +1,12 @@
angular.module('portainer.app')
.factory('Endpoints', ['$resource', 'API_ENDPOINT_ENDPOINTS', function EndpointsFactory($resource, API_ENDPOINT_ENDPOINTS) {
'use strict';
return $resource(API_ENDPOINT_ENDPOINTS + '/:id/:action', {}, {
create: { method: 'POST', ignoreLoadingBar: true },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } },
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
remove: { method: 'DELETE', params: { id: '@id'} }
});
}]);

View file

@ -0,0 +1,12 @@
angular.module('portainer.app')
.factory('Registries', ['$resource', 'API_ENDPOINT_REGISTRIES', function RegistriesFactory($resource, API_ENDPOINT_REGISTRIES) {
'use strict';
return $resource(API_ENDPOINT_REGISTRIES + '/:id/:action', {}, {
create: { method: 'POST', ignoreLoadingBar: true },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } },
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
remove: { method: 'DELETE', params: { id: '@id'} }
});
}]);

View file

@ -0,0 +1,10 @@
angular.module('portainer.app')
.factory('ResourceControl', ['$resource', 'API_ENDPOINT_RESOURCE_CONTROLS', function ResourceControlFactory($resource, API_ENDPOINT_RESOURCE_CONTROLS) {
'use strict';
return $resource(API_ENDPOINT_RESOURCE_CONTROLS + '/:id', {}, {
create: { method: 'POST', ignoreLoadingBar: true },
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } },
remove: { method: 'DELETE', params: { id: '@id'} }
});
}]);

View file

@ -0,0 +1,10 @@
angular.module('portainer.app')
.factory('Settings', ['$resource', 'API_ENDPOINT_SETTINGS', function SettingsFactory($resource, API_ENDPOINT_SETTINGS) {
'use strict';
return $resource(API_ENDPOINT_SETTINGS + '/:subResource/:action', {}, {
get: { method: 'GET' },
update: { method: 'PUT', ignoreLoadingBar: true },
publicSettings: { method: 'GET', params: { subResource: 'public' }, ignoreLoadingBar: true },
checkLDAPConnectivity: { method: 'PUT', params: { subResource: 'authentication', action: 'checkLDAP' } }
});
}]);

View file

@ -0,0 +1,15 @@
angular.module('portainer.app')
.factory('Stack', ['$resource', 'EndpointProvider', 'API_ENDPOINT_ENDPOINTS', function StackFactory($resource, EndpointProvider, API_ENDPOINT_ENDPOINTS) {
'use strict';
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/stacks/:id/:action', {
endpointId: EndpointProvider.endpointID
},
{
get: { method: 'GET', params: { id: '@id' } },
query: { method: 'GET', isArray: true },
create: { method: 'POST', ignoreLoadingBar: true },
update: { method: 'PUT', params: { id: '@id' }, ignoreLoadingBar: true },
remove: { method: 'DELETE', params: { id: '@id'} },
getStackFile: { method: 'GET', params: { id : '@id', action: 'stackfile' } }
});
}]);

View file

@ -0,0 +1,7 @@
angular.module('portainer.app')
.factory('Status', ['$resource', 'API_ENDPOINT_STATUS', function StatusFactory($resource, API_ENDPOINT_STATUS) {
'use strict';
return $resource(API_ENDPOINT_STATUS, {}, {
get: { method: 'GET' }
});
}]);

View file

@ -0,0 +1,12 @@
angular.module('portainer.app')
.factory('Teams', ['$resource', 'API_ENDPOINT_TEAMS', function TeamsFactory($resource, API_ENDPOINT_TEAMS) {
'use strict';
return $resource(API_ENDPOINT_TEAMS + '/:id/:entity/:entityId', {}, {
create: { method: 'POST', ignoreLoadingBar: true },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } },
remove: { method: 'DELETE', params: { id: '@id'} },
queryMemberships: { method: 'GET', isArray: true, params: { id: '@id', entity: 'memberships' } }
});
}]);

View file

@ -0,0 +1,10 @@
angular.module('portainer.app')
.factory('TeamMemberships', ['$resource', 'API_ENDPOINT_TEAM_MEMBERSHIPS', function TeamMembershipsFactory($resource, API_ENDPOINT_TEAM_MEMBERSHIPS) {
'use strict';
return $resource(API_ENDPOINT_TEAM_MEMBERSHIPS + '/:id/:action', {}, {
create: { method: 'POST', ignoreLoadingBar: true },
query: { method: 'GET', isArray: true },
update: { method: 'PUT', params: { id: '@id' } },
remove: { method: 'DELETE', params: { id: '@id'} }
});
}]);

View file

@ -0,0 +1,6 @@
angular.module('portainer.app')
.factory('Template', ['$resource', 'API_ENDPOINT_TEMPLATES', function TemplateFactory($resource, API_ENDPOINT_TEMPLATES) {
return $resource(API_ENDPOINT_TEMPLATES, {}, {
get: {method: 'GET', isArray: true}
});
}]);

View file

@ -0,0 +1,16 @@
angular.module('portainer.app')
.factory('Users', ['$resource', 'API_ENDPOINT_USERS', function UsersFactory($resource, API_ENDPOINT_USERS) {
'use strict';
return $resource(API_ENDPOINT_USERS + '/:id/:entity/:entityId', {}, {
create: { method: 'POST', ignoreLoadingBar: true },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' }, ignoreLoadingBar: true },
remove: { method: 'DELETE', params: { id: '@id'} },
queryMemberships: { method: 'GET', isArray: true, params: { id: '@id', entity: 'memberships' } },
// RPCs should be moved to a specific endpoint
checkPassword: { method: 'POST', params: { id: '@id', entity: 'passwd' }, ignoreLoadingBar: true },
checkAdminUser: { method: 'GET', params: { id: 'admin', entity: 'check' }, isArray: true, ignoreLoadingBar: true },
initAdminUser: { method: 'POST', params: { id: 'admin', entity: 'init' }, ignoreLoadingBar: true }
});
}]);

View file

@ -0,0 +1,58 @@
angular.module('portainer.app')
.factory('AccessService', ['$q', 'UserService', 'TeamService', function AccessServiceFactory($q, UserService, TeamService) {
'use strict';
var service = {};
function mapAccessDataFromAuthorizedIDs(userAccesses, teamAccesses, authorizedUserIDs, authorizedTeamIDs) {
var accesses = [];
var authorizedAccesses = [];
angular.forEach(userAccesses, function(access) {
if (_.includes(authorizedUserIDs, access.Id)) {
authorizedAccesses.push(access);
} else {
accesses.push(access);
}
});
angular.forEach(teamAccesses, function(access) {
if (_.includes(authorizedTeamIDs, access.Id)) {
authorizedAccesses.push(access);
} else {
accesses.push(access);
}
});
return {
accesses: accesses,
authorizedAccesses: authorizedAccesses
};
}
service.accesses = function(authorizedUserIDs, authorizedTeamIDs) {
var deferred = $q.defer();
$q.all({
users: UserService.users(false),
teams: TeamService.teams()
})
.then(function success(data) {
var userAccesses = data.users.map(function (user) {
return new UserAccessViewModel(user);
});
var teamAccesses = data.teams.map(function (team) {
return new TeamAccessViewModel(team);
});
var accessData = mapAccessDataFromAuthorizedIDs(userAccesses, teamAccesses, authorizedUserIDs, authorizedTeamIDs);
deferred.resolve(accessData);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve users and teams', err: err });
});
return deferred.promise;
};
return service;
}]);

View file

@ -0,0 +1,26 @@
angular.module('portainer.app')
.factory('DockerHubService', ['$q', 'DockerHub', function DockerHubServiceFactory($q, DockerHub) {
'use strict';
var service = {};
service.dockerhub = function() {
var deferred = $q.defer();
DockerHub.get().$promise
.then(function success(data) {
var dockerhub = new DockerHubViewModel(data);
deferred.resolve(dockerhub);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve DockerHub details', err: err });
});
return deferred.promise;
};
service.update = function(dockerhub) {
return DockerHub.update({}, dockerhub).$promise;
};
return service;
}]);

View file

@ -0,0 +1,92 @@
angular.module('portainer.app')
.factory('EndpointService', ['$q', 'Endpoints', 'FileUploadService', function EndpointServiceFactory($q, Endpoints, FileUploadService) {
'use strict';
var service = {};
service.endpoint = function(endpointID) {
return Endpoints.get({id: endpointID}).$promise;
};
service.endpoints = function() {
return Endpoints.query({}).$promise;
};
service.updateAccess = function(id, authorizedUserIDs, authorizedTeamIDs) {
return Endpoints.updateAccess({id: id}, {authorizedUsers: authorizedUserIDs, authorizedTeams: authorizedTeamIDs}).$promise;
};
service.updateEndpoint = function(id, endpointParams) {
var query = {
name: endpointParams.name,
PublicURL: endpointParams.PublicURL,
TLS: endpointParams.TLS,
TLSSkipVerify: endpointParams.TLSSkipVerify,
TLSSkipClientVerify: endpointParams.TLSSkipClientVerify,
authorizedUsers: endpointParams.authorizedUsers
};
if (endpointParams.type && endpointParams.URL) {
query.URL = endpointParams.type === 'local' ? ('unix://' + endpointParams.URL) : ('tcp://' + endpointParams.URL);
}
var deferred = $q.defer();
FileUploadService.uploadTLSFilesForEndpoint(id, endpointParams.TLSCACert, endpointParams.TLSCert, endpointParams.TLSKey)
.then(function success() {
deferred.notify({upload: false});
return Endpoints.update({id: id}, query).$promise;
})
.then(function success(data) {
deferred.resolve(data);
})
.catch(function error(err) {
deferred.notify({upload: false});
deferred.reject({msg: 'Unable to update endpoint', err: err});
});
return deferred.promise;
};
service.deleteEndpoint = function(endpointID) {
return Endpoints.remove({id: endpointID}).$promise;
};
service.createLocalEndpoint = function(name, URL, TLS, active) {
var endpoint = {
Name: 'local',
URL: 'unix:///var/run/docker.sock',
TLS: false
};
return Endpoints.create({}, endpoint).$promise;
};
service.createRemoteEndpoint = function(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
var endpoint = {
Name: name,
URL: 'tcp://' + URL,
PublicURL: PublicURL,
TLS: TLS,
TLSSkipVerify: TLSSkipVerify,
TLSSkipClientVerify: TLSSkipClientVerify
};
var deferred = $q.defer();
Endpoints.create({}, endpoint).$promise
.then(function success(data) {
var endpointID = data.Id;
if (!TLSSkipVerify || !TLSSkipClientVerify) {
deferred.notify({upload: true});
FileUploadService.uploadTLSFilesForEndpoint(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile)
.then(function success() {
deferred.notify({upload: false});
deferred.resolve(data);
});
} else {
deferred.resolve(data);
}
})
.catch(function error(err) {
deferred.notify({upload: false});
deferred.reject({msg: 'Unable to upload TLS certs', err: err});
});
return deferred.promise;
};
return service;
}]);

View file

@ -0,0 +1,92 @@
angular.module('portainer.app')
.factory('RegistryService', ['$q', 'Registries', 'DockerHubService', 'RegistryHelper', 'ImageHelper', function RegistryServiceFactory($q, Registries, DockerHubService, RegistryHelper, ImageHelper) {
'use strict';
var service = {};
service.registries = function() {
var deferred = $q.defer();
Registries.query().$promise
.then(function success(data) {
var registries = data.map(function (item) {
return new RegistryViewModel(item);
});
deferred.resolve(registries);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve registries', err: err});
});
return deferred.promise;
};
service.registry = function(id) {
var deferred = $q.defer();
Registries.get({id: id}).$promise
.then(function success(data) {
var registry = new RegistryViewModel(data);
deferred.resolve(registry);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve registry details', err: err});
});
return deferred.promise;
};
service.encodedCredentials = function(registry) {
var credentials = {
username: registry.Username,
password: registry.Password,
serveraddress: registry.URL
};
return btoa(JSON.stringify(credentials));
};
service.updateAccess = function(id, authorizedUserIDs, authorizedTeamIDs) {
return Registries.updateAccess({id: id}, {authorizedUsers: authorizedUserIDs, authorizedTeams: authorizedTeamIDs}).$promise;
};
service.deleteRegistry = function(id) {
return Registries.remove({id: id}).$promise;
};
service.updateRegistry = function(registry) {
return Registries.update({ id: registry.Id }, registry).$promise;
};
service.createRegistry = function(name, URL, authentication, username, password) {
var payload = {
Name: name,
URL: URL,
Authentication: authentication
};
if (authentication) {
payload.Username = username;
payload.Password = password;
}
return Registries.create({}, payload).$promise;
};
service.retrieveRegistryFromRepository = function(repository) {
var deferred = $q.defer();
var imageDetails = ImageHelper.extractImageAndRegistryFromRepository(repository);
$q.when(imageDetails.registry ? service.registries() : DockerHubService.dockerhub())
.then(function success(data) {
var registry = data;
if (imageDetails.registry) {
registry = RegistryHelper.getRegistryByURL(data, imageDetails.registry);
}
deferred.resolve(registry);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve the registry associated to the repository', err: err });
});
return deferred.promise;
};
return service;
}]);

View file

@ -0,0 +1,125 @@
angular.module('portainer.app')
.factory('ResourceControlService', ['$q', 'ResourceControl', 'UserService', 'TeamService', 'ResourceControlHelper', function ResourceControlServiceFactory($q, ResourceControl, UserService, TeamService, ResourceControlHelper) {
'use strict';
var service = {};
service.createResourceControl = function(administratorsOnly, userIDs, teamIDs, resourceID, type, subResourceIDs) {
var payload = {
Type: type,
AdministratorsOnly: administratorsOnly,
ResourceID: resourceID,
Users: userIDs,
Teams: teamIDs,
SubResourceIDs: subResourceIDs
};
return ResourceControl.create({}, payload).$promise;
};
service.deleteResourceControl = function(rcID) {
return ResourceControl.remove({id: rcID}).$promise;
};
service.updateResourceControl = function(admin, userIDs, teamIDs, resourceControlId) {
var payload = {
AdministratorsOnly: admin,
Users: userIDs,
Teams: teamIDs
};
return ResourceControl.update({id: resourceControlId}, payload).$promise;
};
service.applyResourceControl = function(resourceControlType, resourceIdentifier, userId, accessControlData, subResources) {
if (!accessControlData.AccessControlEnabled) {
return;
}
var authorizedUserIds = [];
var authorizedTeamIds = [];
var administratorsOnly = false;
switch (accessControlData.Ownership) {
case 'administrators':
administratorsOnly = true;
break;
case 'private':
authorizedUserIds.push(userId);
break;
case 'restricted':
angular.forEach(accessControlData.AuthorizedUsers, function(user) {
authorizedUserIds.push(user.Id);
});
angular.forEach(accessControlData.AuthorizedTeams, function(team) {
authorizedTeamIds.push(team.Id);
});
break;
}
return service.createResourceControl(administratorsOnly, authorizedUserIds,
authorizedTeamIds, resourceIdentifier, resourceControlType, subResources);
};
service.applyResourceControlChange = function(resourceControlType, resourceId, resourceControl, ownershipParameters) {
if (resourceControl) {
if (ownershipParameters.ownership === 'public') {
return service.deleteResourceControl(resourceControl.Id);
} else {
return service.updateResourceControl(ownershipParameters.administratorsOnly, ownershipParameters.authorizedUserIds,
ownershipParameters.authorizedTeamIds, resourceControl.Id);
}
} else {
return service.createResourceControl(ownershipParameters.administratorsOnly, ownershipParameters.authorizedUserIds,
ownershipParameters.authorizedTeamIds, resourceId, resourceControlType);
}
};
service.retrieveOwnershipDetails = function(resourceControl) {
var deferred = $q.defer();
if (!resourceControl) {
deferred.resolve({ authorizedUsers: [], authorizedTeams: [] });
return deferred.promise;
}
$q.all({
users: resourceControl.UserAccesses.length > 0 ? UserService.users(false) : [],
teams: resourceControl.TeamAccesses.length > 0 ? TeamService.teams() : []
})
.then(function success(data) {
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 });
});
return deferred.promise;
};
service.retrieveUserPermissionsOnResource = function(userID, isAdministrator, resourceControl) {
var deferred = $q.defer();
if (!resourceControl || isAdministrator) {
deferred.resolve({ isPartOfRestrictedUsers: false, isLeaderOfAnyRestrictedTeams: false });
return deferred.promise;
}
var found = _.find(resourceControl.UserAccesses, { UserId: userID });
if (found) {
deferred.resolve({ isPartOfRestrictedUsers: true, isLeaderOfAnyRestrictedTeams: false });
} else {
var isTeamLeader = false;
UserService.userMemberships(userID)
.then(function success(data) {
var memberships = data;
isTeamLeader = ResourceControlHelper.isLeaderOfAnyRestrictedTeams(memberships, resourceControl);
deferred.resolve({ isPartOfRestrictedUsers: false, isLeaderOfAnyRestrictedTeams: isTeamLeader });
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve user memberships', err: err });
});
}
return deferred.promise;
};
return service;
}]);

View file

@ -0,0 +1,45 @@
angular.module('portainer.app')
.factory('SettingsService', ['$q', 'Settings', function SettingsServiceFactory($q, Settings) {
'use strict';
var service = {};
service.settings = function() {
var deferred = $q.defer();
Settings.get().$promise
.then(function success(data) {
var settings = new SettingsViewModel(data);
deferred.resolve(settings);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve application settings', err: err });
});
return deferred.promise;
};
service.update = function(settings) {
return Settings.update({}, settings).$promise;
};
service.publicSettings = function() {
var deferred = $q.defer();
Settings.publicSettings().$promise
.then(function success(data) {
var settings = new SettingsViewModel(data);
deferred.resolve(settings);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve application settings', err: err });
});
return deferred.promise;
};
service.checkLDAPConnectivity = function(settings) {
return Settings.checkLDAPConnectivity({}, settings).$promise;
};
return service;
}]);

View file

@ -0,0 +1,22 @@
angular.module('portainer.app')
.factory('StatusService', ['$q', 'Status', function StatusServiceFactory($q, Status) {
'use strict';
var service = {};
service.status = function() {
var deferred = $q.defer();
Status.get().$promise
.then(function success(data) {
var status = new StatusViewModel(data);
deferred.resolve(status);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve application status', err: err });
});
return deferred.promise;
};
return service;
}]);

View file

@ -0,0 +1,44 @@
angular.module('portainer.app')
.factory('TeamMembershipService', ['$q', 'TeamMemberships', function TeamMembershipFactory($q, TeamMemberships) {
'use strict';
var service = {};
service.memberships = function() {
var deferred = $q.defer();
TeamMemberships.query().$promise
.then(function success(data) {
var memberships = data.map(function (item) {
return new TeamMembershipModel(item);
});
deferred.resolve(memberships);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve team memberships', err: err});
});
return deferred.promise;
};
service.createMembership = function(userId, teamId, role) {
var payload = {
UserID: userId,
TeamID: teamId,
Role: role
};
return TeamMemberships.create({}, payload).$promise;
};
service.deleteMembership = function(id) {
return TeamMemberships.remove({id: id}).$promise;
};
service.updateMembership = function(id, userId, teamId, role) {
var payload = {
UserID: userId,
TeamID: teamId,
Role: role
};
return TeamMemberships.update({id: id}, payload).$promise;
};
return service;
}]);

View file

@ -0,0 +1,84 @@
angular.module('portainer.app')
.factory('TeamService', ['$q', 'Teams', 'TeamMembershipService', function TeamServiceFactory($q, Teams, TeamMembershipService) {
'use strict';
var service = {};
service.teams = function() {
var deferred = $q.defer();
Teams.query().$promise
.then(function success(data) {
var teams = data.map(function (item) {
return new TeamViewModel(item);
});
deferred.resolve(teams);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve teams', err: err});
});
return deferred.promise;
};
service.team = function(id) {
var deferred = $q.defer();
Teams.get({id: id}).$promise
.then(function success(data) {
var team = new TeamViewModel(data);
deferred.resolve(team);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve team details', err: err});
});
return deferred.promise;
};
service.createTeam = function(name, leaderIds) {
var deferred = $q.defer();
var payload = {
Name: name
};
Teams.create({}, payload).$promise
.then(function success(data) {
var teamId = data.Id;
var teamMembershipQueries = [];
angular.forEach(leaderIds, function(userId) {
teamMembershipQueries.push(TeamMembershipService.createMembership(userId, teamId, 1));
});
$q.all(teamMembershipQueries)
.then(function success() {
deferred.resolve();
});
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to create team', err: err });
});
return deferred.promise;
};
service.deleteTeam = function(id) {
return Teams.remove({id: id}).$promise;
};
service.updateTeam = function(id, name, members, leaders) {
var payload = {
Name: name
};
return Teams.update({id: id}, payload).$promise;
};
service.userMemberships = function(id) {
var deferred = $q.defer();
Teams.queryMemberships({id: id}).$promise
.then(function success(data) {
var memberships = data.map(function (item) {
return new TeamMembershipModel(item);
});
deferred.resolve(memberships);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve user memberships for the team', err: err });
});
return deferred.promise;
};
return service;
}]);

View file

@ -0,0 +1,159 @@
angular.module('portainer.app')
.factory('UserService', ['$q', 'Users', 'UserHelper', 'TeamService', 'TeamMembershipService', function UserServiceFactory($q, Users, UserHelper, TeamService, TeamMembershipService) {
'use strict';
var service = {};
service.users = function(includeAdministrators) {
var deferred = $q.defer();
Users.query({}).$promise
.then(function success(data) {
var users = data.map(function (user) {
return new UserViewModel(user);
});
if (!includeAdministrators) {
users = UserHelper.filterNonAdministratorUsers(users);
}
deferred.resolve(users);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve users', err: err });
});
return deferred.promise;
};
service.user = function(id) {
var deferred = $q.defer();
Users.get({id: id}).$promise
.then(function success(data) {
var user = new UserViewModel(data);
deferred.resolve(user);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve user details', err: err });
});
return deferred.promise;
};
service.createUser = function(username, password, role, teamIds) {
var deferred = $q.defer();
Users.create({}, {username: username, password: password, role: role}).$promise
.then(function success(data) {
var userId = data.Id;
var teamMembershipQueries = [];
angular.forEach(teamIds, function(teamId) {
teamMembershipQueries.push(TeamMembershipService.createMembership(userId, teamId, 2));
});
$q.all(teamMembershipQueries)
.then(function success() {
deferred.resolve();
});
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to create user', err: err });
});
return deferred.promise;
};
service.deleteUser = function(id) {
return Users.remove({id: id}).$promise;
};
service.updateUser = function(id, password, role) {
var query = {
password: password,
role: role
};
return Users.update({id: id}, query).$promise;
};
service.updateUserPassword = function(id, currentPassword, newPassword) {
var deferred = $q.defer();
Users.checkPassword({id: id}, {password: currentPassword}).$promise
.then(function success(data) {
if (!data.valid) {
deferred.reject({invalidPassword: true});
} else {
return service.updateUser(id, newPassword, undefined);
}
})
.then(function success(data) {
deferred.resolve();
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to update user password', err: err});
});
return deferred.promise;
};
service.userMemberships = function(id) {
var deferred = $q.defer();
Users.queryMemberships({id: id}).$promise
.then(function success(data) {
var memberships = data.map(function (item) {
return new TeamMembershipModel(item);
});
deferred.resolve(memberships);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve user memberships', err: err });
});
return deferred.promise;
};
service.userLeadingTeams = function(id) {
var deferred = $q.defer();
$q.all({
teams: TeamService.teams(),
memberships: service.userMemberships(id)
})
.then(function success(data) {
var memberships = data.memberships;
var teams = data.teams.filter(function (team) {
var membership = _.find(memberships, {TeamId: team.Id});
if (membership && membership.Role === 1) {
return team;
}
});
deferred.resolve(teams);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve user teams', err: err });
});
return deferred.promise;
};
service.initAdministrator = function(username, password) {
return Users.initAdminUser({ Username: username, Password: password }).$promise;
};
service.administratorExists = function() {
var deferred = $q.defer();
Users.checkAdminUser({}).$promise
.then(function success(data) {
deferred.resolve(true);
})
.catch(function error(err) {
if (err.status === 404) {
deferred.resolve(false);
}
deferred.reject({ msg: 'Unable to verify administrator account existence', err: err });
});
return deferred.promise;
};
return service;
}]);

View file

@ -0,0 +1,44 @@
angular.module('portainer.app')
.factory('Authentication', ['$q', 'Auth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider', function AuthenticationFactory($q, Auth, jwtHelper, LocalStorage, StateManager, EndpointProvider) {
'use strict';
var user = {};
return {
init: function() {
var jwt = LocalStorage.getJWT();
if (jwt) {
var tokenPayload = jwtHelper.decodeToken(jwt);
user.username = tokenPayload.username;
user.ID = tokenPayload.id;
user.role = tokenPayload.role;
}
},
login: function(username, password) {
return $q(function (resolve, reject) {
Auth.login({username: username, password: password}).$promise
.then(function(data) {
LocalStorage.storeJWT(data.jwt);
var tokenPayload = jwtHelper.decodeToken(data.jwt);
user.username = username;
user.ID = tokenPayload.id;
user.role = tokenPayload.role;
resolve();
}, function() {
reject();
});
});
},
logout: function() {
StateManager.clean();
EndpointProvider.clean();
LocalStorage.clean();
},
isAuthenticated: function() {
var jwt = LocalStorage.getJWT();
return jwt && !jwtHelper.isTokenExpired(jwt);
},
getUserDetails: function() {
return user;
}
};
}]);

View file

@ -0,0 +1,167 @@
angular.module('portainer.app')
.factory('ChartService', [function ChartService() {
'use strict';
// Max. number of items to display on a chart
var CHART_LIMIT = 600;
var service = {};
function defaultChartOptions(pos, tooltipCallback, scalesCallback) {
return {
animation: { duration: 0 },
responsiveAnimationDuration: 0,
responsive: true,
tooltips: {
mode: 'index',
intersect: false,
position: pos,
callbacks: {
label: function(tooltipItem, data) {
var datasetLabel = data.datasets[tooltipItem.datasetIndex].label;
return tooltipCallback(datasetLabel, tooltipItem.yLabel);
}
}
},
hover: { animationDuration: 0 },
scales: {
yAxes: [{
ticks: {
beginAtZero: true,
callback: scalesCallback
}
}]
}
};
}
function CreateChart (context, label, tooltipCallback, scalesCallback) {
return new Chart(context, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: label,
data: [],
fill: true,
backgroundColor: 'rgba(151,187,205,0.4)',
borderColor: 'rgba(151,187,205,0.6)',
pointBackgroundColor: 'rgba(151,187,205,1)',
pointBorderColor: 'rgba(151,187,205,1)',
pointRadius: 2,
borderWidth: 2
}
]
},
options: defaultChartOptions('nearest', tooltipCallback, scalesCallback)
});
}
service.CreateCPUChart = function(context) {
return CreateChart(context, 'CPU', percentageBasedTooltipLabel, percentageBasedAxisLabel);
};
service.CreateMemoryChart = function(context) {
return CreateChart(context, 'Memory', byteBasedTooltipLabel, byteBasedAxisLabel);
};
service.CreateNetworkChart = function(context) {
return new Chart(context, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'RX on eth0',
data: [],
fill: false,
backgroundColor: 'rgba(151,187,205,0.4)',
borderColor: 'rgba(151,187,205,0.6)',
pointBackgroundColor: 'rgba(151,187,205,1)',
pointBorderColor: 'rgba(151,187,205,1)',
pointRadius: 2,
borderWidth: 2
},
{
label: 'TX on eth0',
data: [],
fill: false,
backgroundColor: 'rgba(255,180,174,0.4)',
borderColor: 'rgba(255,180,174,0.6)',
pointBackgroundColor: 'rgba(255,180,174,1)',
pointBorderColor: 'rgba(255,180,174,1)',
pointRadius: 2,
borderWidth: 2
}
]
},
options: defaultChartOptions('average', byteBasedTooltipLabel, byteBasedAxisLabel)
});
};
function UpdateChart(label, value, chart) {
chart.data.labels.push(label);
chart.data.datasets[0].data.push(value);
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
chart.data.labels.pop();
chart.data.datasets[0].data.pop();
}
chart.update(0);
}
service.UpdateMemoryChart = UpdateChart;
service.UpdateCPUChart = UpdateChart;
service.UpdateNetworkChart = function(label, rx, tx, chart) {
chart.data.labels.push(label);
chart.data.datasets[0].data.push(rx);
chart.data.datasets[1].data.push(tx);
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
chart.data.labels.pop();
chart.data.datasets[0].data.pop();
chart.data.datasets[1].data.pop();
}
chart.update(0);
};
function byteBasedTooltipLabel(label, value) {
var processedValue = 0;
if (value > 5) {
processedValue = filesize(value, {base: 10, round: 1});
} else {
processedValue = value.toFixed(1) + 'B';
}
return label + ': ' + processedValue;
}
function byteBasedAxisLabel(value, index, values) {
if (value > 5) {
return filesize(value, {base: 10, round: 1});
}
return value.toFixed(1) + 'B';
}
function percentageBasedAxisLabel(value, index, values) {
if (value > 1) {
return Math.round(value) + '%';
}
return value.toFixed(1) + '%';
}
function percentageBasedTooltipLabel(label, value) {
var processedValue = 0;
if (value > 1) {
processedValue = Math.round(value);
} else {
processedValue = value.toFixed(1);
}
return label + ': ' + processedValue + '%';
}
return service;
}]);

View file

@ -0,0 +1,34 @@
angular.module('portainer.app')
.factory('CodeMirrorService', function CodeMirrorService() {
'use strict';
var codeMirrorGenericOptions = {
lineNumbers: true
};
var codeMirrorYAMLOptions = {
mode: 'text/x-yaml',
gutters: ['CodeMirror-lint-markers'],
lint: true
};
var service = {};
service.applyCodeMirrorOnElement = function(element, yamlLint, readOnly) {
var options = codeMirrorGenericOptions;
if (yamlLint) {
options = codeMirrorYAMLOptions;
}
if (readOnly) {
options.readOnly = true;
}
var cm = CodeMirror.fromTextArea(element, options);
cm.setSize('100%', 500);
return cm;
};
return service;
});

View file

@ -0,0 +1,37 @@
angular.module('portainer.app')
.factory('DatatableService', ['LocalStorage',
function DatatableServiceFactory(LocalStorage) {
'use strict';
var service = {};
service.setDataTableSettings = function(key, settings) {
LocalStorage.storeDataTableSettings(key, settings);
};
service.getDataTableSettings = function(key) {
return LocalStorage.getDataTableSettings(key);
};
service.setDataTableFilters = function(key, filters) {
LocalStorage.storeDataTableFilters(key, filters);
};
service.getDataTableFilters = function(key) {
return LocalStorage.getDataTableFilters(key);
};
service.getDataTableOrder = function(key) {
return LocalStorage.getDataTableOrder(key);
};
service.setDataTableOrder = function(key, orderBy, reverse) {
var filter = {
orderBy: orderBy,
reverse: reverse
};
LocalStorage.storeDataTableOrder(key, filter);
};
return service;
}]);

View file

@ -0,0 +1,41 @@
angular.module('portainer.app')
.factory('EndpointProvider', ['LocalStorage', function EndpointProviderFactory(LocalStorage) {
'use strict';
var service = {};
var endpoint = {};
service.initialize = function() {
var endpointID = LocalStorage.getEndpointID();
var endpointPublicURL = LocalStorage.getEndpointPublicURL();
if (endpointID) {
endpoint.ID = endpointID;
}
if (endpointPublicURL) {
endpoint.PublicURL = endpointPublicURL;
}
};
service.clean = function() {
endpoint = {};
};
service.endpointID = function() {
return endpoint.ID;
};
service.setEndpointID = function(id) {
endpoint.ID = id;
LocalStorage.storeEndpointID(id);
};
service.endpointPublicURL = function() {
return endpoint.PublicURL;
};
service.setEndpointPublicURL = function(publicURL) {
endpoint.PublicURL = publicURL;
LocalStorage.storeEndpointPublicURL(publicURL);
};
return service;
}]);

View file

@ -0,0 +1,35 @@
angular.module('portainer.app')
.factory('ExtensionManager', ['$q', 'PluginService', 'StoridgeManager', function ExtensionManagerFactory($q, PluginService, StoridgeManager) {
'use strict';
var service = {};
service.init = function() {
return $q.all(
StoridgeManager.init()
);
};
service.reset = function() {
StoridgeManager.reset();
};
service.extensions = function() {
var deferred = $q.defer();
var extensions = [];
PluginService.volumePlugins()
.then(function success(data) {
var volumePlugins = data;
if (_.includes(volumePlugins, 'cio:latest')) {
extensions.push('storidge');
}
})
.finally(function final() {
deferred.resolve(extensions);
});
return deferred.promise;
};
return service;
}]);

View file

@ -0,0 +1,58 @@
angular.module('portainer.app')
.factory('FileUploadService', ['$q', 'Upload', 'EndpointProvider', function FileUploadFactory($q, Upload, EndpointProvider) {
'use strict';
var service = {};
function uploadFile(url, file) {
return Upload.upload({ url: url, data: { file: file }});
}
service.createStack = function(stackName, swarmId, file, env) {
var endpointID = EndpointProvider.endpointID();
return Upload.upload({
url: 'api/endpoints/' + endpointID + '/stacks?method=file',
data: {
file: file,
Name: stackName,
SwarmID: swarmId,
Env: Upload.json(env)
},
ignoreLoadingBar: true
});
};
service.uploadLDAPTLSFiles = function(TLSCAFile, TLSCertFile, TLSKeyFile) {
var queue = [];
if (TLSCAFile) {
queue.push(uploadFile('api/upload/tls/ca?folder=ldap', TLSCAFile));
}
if (TLSCertFile) {
queue.push(uploadFile('api/upload/tls/cert?folder=ldap', TLSCertFile));
}
if (TLSKeyFile) {
queue.push(uploadFile('api/upload/tls/key?folder=ldap', TLSKeyFile));
}
return $q.all(queue);
};
service.uploadTLSFilesForEndpoint = function(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile) {
var queue = [];
if (TLSCAFile) {
queue.push(uploadFile('api/upload/tls/ca?folder=' + endpointID, TLSCAFile));
}
if (TLSCertFile) {
queue.push(uploadFile('api/upload/tls/cert?folder=' + endpointID, TLSCertFile));
}
if (TLSKeyFile) {
queue.push(uploadFile('api/upload/tls/key?folder=' + endpointID, TLSKeyFile));
}
return $q.all(queue);
};
return service;
}]);

View file

@ -0,0 +1,24 @@
angular.module('portainer.app')
.factory('FormValidator', [function FormValidatorFactory() {
'use strict';
var validator = {};
validator.validateAccessControl = function(accessControlData, isAdmin) {
if (!accessControlData.AccessControlEnabled) {
return '';
}
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) {
return 'You must specify at least a team.';
}
return '';
};
return validator;
}]);

View file

@ -0,0 +1,17 @@
angular.module('portainer.app')
.factory('HttpRequestHelper', [function HttpRequestHelper() {
'use strict';
var service = {};
var headers = {};
service.registryAuthenticationHeader = function() {
return headers.registryAuthentication;
};
service.setRegistryAuthenticationHeader = function(headerValue) {
headers.registryAuthentication = headerValue;
};
return service;
}]);

View file

@ -0,0 +1,75 @@
angular.module('portainer.app')
.factory('LocalStorage', ['localStorageService', function LocalStorageFactory(localStorageService) {
'use strict';
return {
storeEndpointID: function(id) {
localStorageService.set('ENDPOINT_ID', id);
},
getEndpointID: function() {
return localStorageService.get('ENDPOINT_ID');
},
storeEndpointPublicURL: function(publicURL) {
localStorageService.set('ENDPOINT_PUBLIC_URL', publicURL);
},
getEndpointPublicURL: function() {
return localStorageService.get('ENDPOINT_PUBLIC_URL');
},
storeEndpointState: function(state) {
localStorageService.set('ENDPOINT_STATE', state);
},
getEndpointState: function() {
return localStorageService.get('ENDPOINT_STATE');
},
storeApplicationState: function(state) {
localStorageService.set('APPLICATION_STATE', state);
},
getApplicationState: function() {
return localStorageService.get('APPLICATION_STATE');
},
storeJWT: function(jwt) {
localStorageService.set('JWT', jwt);
},
getJWT: function() {
return localStorageService.get('JWT');
},
deleteJWT: function() {
localStorageService.remove('JWT');
},
storePaginationLimit: function(key, count) {
localStorageService.cookie.set('pagination_' + key, count);
},
getPaginationLimit: function(key) {
return localStorageService.cookie.get('pagination_' + key);
},
storeStoridgeAPIURL: function(url) {
localStorageService.set('STORIDGE_API_URL', url);
},
getStoridgeAPIURL: function() {
return localStorageService.get('STORIDGE_API_URL');
},
clearStoridgeAPIURL: function() {
return localStorageService.remove('STORIDGE_API_URL');
},
getDataTableOrder: function(key) {
return localStorageService.get('datatable_order_' + key);
},
storeDataTableOrder: function(key, data) {
localStorageService.set('datatable_order_' + key, data);
},
getDataTableFilters: function(key) {
return localStorageService.get('datatable_filters_' + key);
},
storeDataTableFilters: function(key, data) {
localStorageService.set('datatable_filters_' + key, data);
},
getDataTableSettings: function(key) {
return localStorageService.get('datatable_settings_' + key);
},
storeDataTableSettings: function(key, data) {
localStorageService.set('datatable_settings_' + key, data);
},
clean: function() {
localStorageService.clearAll();
}
};
}]);

View file

@ -0,0 +1,174 @@
angular.module('portainer.app')
.factory('ModalService', [function ModalServiceFactory() {
'use strict';
var service = {};
var applyBoxCSS = function(box) {
box.css({
'top': '50%',
'margin-top': function () {
return -(box.height() / 2);
}
});
};
var confirmButtons = function(options) {
var buttons = {
confirm: {
label: options.buttons.confirm.label,
className: options.buttons.confirm.className
},
cancel: {
label: options.buttons.cancel && options.buttons.cancel.label ? options.buttons.cancel.label : 'Cancel'
}
};
return buttons;
};
service.confirm = function(options){
var box = bootbox.confirm({
title: options.title,
message: options.message,
buttons: confirmButtons(options),
callback: options.callback
});
applyBoxCSS(box);
};
service.prompt = function(options){
var box = bootbox.prompt({
title: options.title,
inputType: options.inputType,
inputOptions: options.inputOptions,
buttons: confirmButtons(options),
callback: options.callback
});
applyBoxCSS(box);
};
service.customPrompt = function(options) {
var box = bootbox.prompt({
title: options.title,
inputType: options.inputType,
inputOptions: options.inputOptions,
buttons: confirmButtons(options),
callback: options.callback
});
applyBoxCSS(box);
box.find('.bootbox-body').prepend('<p>' + options.message + '</p>');
box.find('.bootbox-input-checkbox').prop('checked', true);
};
service.confirmAccessControlUpdate = function(callback, msg) {
service.confirm({
title: 'Are you sure ?',
message: 'Changing the ownership of this resource will potentially restrict its management to some users.',
buttons: {
confirm: {
label: 'Change ownership',
className: 'btn-primary'
}
},
callback: callback
});
};
service.confirmImageForceRemoval = function(callback) {
service.confirm({
title: 'Are you sure?',
message: 'Forcing the removal of the image will remove the image even if it has multiple tags or if it is used by stopped containers.',
buttons: {
confirm: {
label: 'Remove the image',
className: 'btn-danger'
}
},
callback: callback
});
};
service.confirmDeletion = function(message, callback) {
service.confirm({
title: 'Are you sure ?',
message: message,
buttons: {
confirm: {
label: 'Remove',
className: 'btn-danger'
}
},
callback: callback
});
};
service.confirmContainerDeletion = function(title, callback) {
service.prompt({
title: title,
inputType: 'checkbox',
inputOptions: [
{
text: 'Automatically remove non-persistent volumes<i></i>',
value: '1'
}
],
buttons: {
confirm: {
label: 'Remove',
className: 'btn-danger'
}
},
callback: callback
});
};
service.confirmContainerRecreation = function(callback) {
service.customPrompt({
title: 'Are you sure?',
message: 'You\'re about to re-create this container, any non-persisted data will be lost. This container will be removed and another one will be created using the same configuration.',
inputType: 'checkbox',
inputOptions: [
{
text: 'Pull latest image<i></i>',
value: '1'
}
],
buttons: {
confirm: {
label: 'Recreate',
className: 'btn-danger'
}
},
callback: callback
});
};
service.confirmExperimentalFeature = function(callback) {
service.confirm({
title: 'Experimental feature',
message: 'This feature is currently experimental, please use with caution.',
buttons: {
confirm: {
label: 'Continue',
className: 'btn-danger'
}
},
callback: callback
});
};
service.confirmServiceForceUpdate = function(message, callback) {
service.confirm({
title: 'Are you sure ?',
message: message,
buttons: {
confirm: {
label: 'Update',
className: 'btn-primary'
}
},
callback: callback
});
};
return service;
}]);

View file

@ -0,0 +1,38 @@
angular.module('portainer.app')
.factory('Notifications', ['$sanitize', function NotificationsFactory($sanitize) {
'use strict';
var service = {};
service.success = function(title, text) {
toastr.success($sanitize(text), $sanitize(title));
};
service.warning = function(title, text) {
toastr.warning($sanitize(text), $sanitize(title), {timeOut: 6000});
};
service.error = function(title, e, fallbackText) {
var msg = fallbackText;
if (e.data && e.data.message) {
msg = e.data.message;
} else if (e.message) {
msg = e.message;
} else if (e.err && e.err.data && e.err.data.message) {
msg = e.err.data.message;
} else if (e.data && e.data.length > 0 && e.data[0].message) {
msg = e.data[0].message;
} else if (e.err && e.err.data && e.err.data.err) {
msg = e.err.data.err;
} else if (e.data && e.data.err) {
msg = e.data.err;
} else if (e.msg) {
msg = e.msg;
}
if (msg !== 'Invalid JWT token') {
toastr.error($sanitize(msg), $sanitize(title), {timeOut: 6000});
}
};
return service;
}]);

View file

@ -0,0 +1,23 @@
angular.module('portainer.app')
.factory('PaginationService', ['LocalStorage', 'PAGINATION_MAX_ITEMS',
function PaginationServiceFactory(LocalStorage, PAGINATION_MAX_ITEMS) {
'use strict';
var service = {};
service.getPaginationLimit = function(key) {
var paginationLimit = PAGINATION_MAX_ITEMS;
var storedLimit = LocalStorage.getPaginationLimit(key);
if (storedLimit !== null) {
paginationLimit = storedLimit;
}
return '' + paginationLimit;
};
service.setPaginationLimit = function(key, limit) {
LocalStorage.storePaginationLimit(key, limit);
};
return service;
}]);

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