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:
parent
e6422a6d75
commit
27dceadba1
354 changed files with 1518 additions and 1755 deletions
266
app/portainer/__module.js
Normal file
266
app/portainer/__module.js
Normal 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);
|
||||
}]);
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -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>
|
|
@ -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();
|
||||
}]);
|
|
@ -0,0 +1,6 @@
|
|||
function AccessControlFormData() {
|
||||
this.AccessControlEnabled = true;
|
||||
this.Ownership = 'private';
|
||||
this.AuthorizedUsers = [];
|
||||
this.AuthorizedTeams = [];
|
||||
}
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -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>
|
|
@ -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();
|
||||
}]);
|
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.app').component('porAccessManagement', {
|
||||
templateUrl: 'app/portainer/components/accessManagement/porAccessManagement.html',
|
||||
controller: 'porAccessManagementController',
|
||||
bindings: {
|
||||
accessControlledEntity: '<',
|
||||
updateAccess: '&'
|
||||
}
|
||||
});
|
|
@ -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>
|
|
@ -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();
|
||||
}]);
|
13
app/portainer/components/autofocus.js
Normal file
13
app/portainer/components/autofocus.js
Normal 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;
|
||||
}]);
|
13
app/portainer/components/buttonSpinner.js
Normal file
13
app/portainer/components/buttonSpinner.js
Normal 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> </span>'
|
||||
};
|
||||
|
||||
return directive;
|
||||
});
|
244
app/portainer/components/datatables/datatable.css
Normal file
244
app/portainer/components/datatables/datatable.css
Normal 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;
|
||||
}
|
|
@ -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>
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}]);
|
|
@ -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>
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -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>
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -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>
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}]);
|
|
@ -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>
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -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>
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -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>
|
|
@ -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();
|
||||
}]);
|
|
@ -0,0 +1,7 @@
|
|||
function EndpointSecurityFormData() {
|
||||
this.TLS = false;
|
||||
this.TLSMode = 'tls_client_ca';
|
||||
this.TLSCACert = null;
|
||||
this.TLSCert = null;
|
||||
this.TLSKey = null;
|
||||
}
|
13
app/portainer/components/header-content.js
Normal file
13
app/portainer/components/header-content.js
Normal 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;
|
||||
}]);
|
17
app/portainer/components/header-title.js
Normal file
17
app/portainer/components/header-title.js
Normal 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;
|
||||
}]);
|
12
app/portainer/components/header.js
Normal file
12
app/portainer/components/header.js
Normal 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;
|
||||
});
|
8
app/portainer/components/loading.js
Normal file
8
app/portainer/components/loading.js
Normal 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;
|
||||
});
|
18
app/portainer/components/onEnterKey.js
Normal file
18
app/portainer/components/onEnterKey.js
Normal 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;
|
||||
}]);
|
3
app/portainer/components/slider/slider.html
Normal file
3
app/portainer/components/slider/slider.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<rzslider rz-slider-options="$ctrl.options" rz-slider-model="$ctrl.model"></rzslider>
|
||||
</div>
|
12
app/portainer/components/slider/slider.js
Normal file
12
app/portainer/components/slider/slider.js
Normal 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: '<'
|
||||
}
|
||||
});
|
22
app/portainer/components/slider/sliderController.js
Normal file
22
app/portainer/components/slider/sliderController.js
Normal 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();
|
||||
}
|
||||
};
|
||||
});
|
12
app/portainer/components/template-widget.js
Normal file
12
app/portainer/components/template-widget.js
Normal 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;
|
||||
});
|
12
app/portainer/components/tooltip.js
Normal file
12
app/portainer/components/tooltip.js
Normal 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;
|
||||
}]);
|
14
app/portainer/components/widget-body.js
Normal file
14
app/portainer/components/widget-body.js
Normal 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;
|
||||
});
|
14
app/portainer/components/widget-custom-header.js
Normal file
14
app/portainer/components/widget-custom-header.js
Normal 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;
|
||||
});
|
10
app/portainer/components/widget-footer.js
Normal file
10
app/portainer/components/widget-footer.js
Normal 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;
|
||||
});
|
15
app/portainer/components/widget-header.js
Normal file
15
app/portainer/components/widget-header.js
Normal 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;
|
||||
});
|
13
app/portainer/components/widget-taskbar.js
Normal file
13
app/portainer/components/widget-taskbar.js
Normal 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;
|
||||
});
|
12
app/portainer/components/widget.js
Normal file
12
app/portainer/components/widget.js
Normal 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;
|
||||
});
|
119
app/portainer/filters/filters.js
Normal file
119
app/portainer/filters/filters.js
Normal 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';
|
||||
}
|
||||
};
|
||||
});
|
18
app/portainer/helpers/formHelper.js
Normal file
18
app/portainer/helpers/formHelper.js
Normal 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;
|
||||
}]);
|
18
app/portainer/helpers/registryHelper.js
Normal file
18
app/portainer/helpers/registryHelper.js
Normal 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;
|
||||
}]);
|
42
app/portainer/helpers/resourceControlHelper.js
Normal file
42
app/portainer/helpers/resourceControlHelper.js
Normal 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;
|
||||
}]);
|
21
app/portainer/helpers/stackHelper.js
Normal file
21
app/portainer/helpers/stackHelper.js
Normal 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;
|
||||
}]);
|
138
app/portainer/helpers/templateHelper.js
Normal file
138
app/portainer/helpers/templateHelper.js
Normal 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;
|
||||
}]);
|
15
app/portainer/helpers/userHelper.js
Normal file
15
app/portainer/helpers/userHelper.js
Normal 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;
|
||||
}]);
|
11
app/portainer/models/access.js
Normal file
11
app/portainer/models/access.js
Normal 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';
|
||||
}
|
7
app/portainer/models/dockerhub.js
Normal file
7
app/portainer/models/dockerhub.js
Normal 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;
|
||||
}
|
11
app/portainer/models/registry.js
Normal file
11
app/portainer/models/registry.js
Normal 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;
|
||||
}
|
19
app/portainer/models/resourceControl.js
Normal file
19
app/portainer/models/resourceControl.js
Normal 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';
|
||||
}
|
||||
}
|
12
app/portainer/models/settings/ldapSettings.js
Normal file
12
app/portainer/models/settings/ldapSettings.js
Normal 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;
|
||||
}
|
11
app/portainer/models/settings/settings.js
Normal file
11
app/portainer/models/settings/settings.js
Normal 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;
|
||||
}
|
6
app/portainer/models/status.js
Normal file
6
app/portainer/models/status.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
function StatusViewModel(data) {
|
||||
this.Authentication = data.Authentication;
|
||||
this.EndpointManagement = data.EndpointManagement;
|
||||
this.Analytics = data.Analytics;
|
||||
this.Version = data.Version;
|
||||
}
|
5
app/portainer/models/team.js
Normal file
5
app/portainer/models/team.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
function TeamViewModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.Name = data.Name;
|
||||
this.Checked = false;
|
||||
}
|
6
app/portainer/models/teamMembership.js
Normal file
6
app/portainer/models/teamMembership.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
function TeamMembershipModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.UserId = data.UserID;
|
||||
this.TeamId = data.TeamID;
|
||||
this.Role = data.Role;
|
||||
}
|
12
app/portainer/models/user.js
Normal file
12
app/portainer/models/user.js
Normal 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;
|
||||
}
|
9
app/portainer/rest/auth.js
Normal file
9
app/portainer/rest/auth.js
Normal 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
|
||||
}
|
||||
});
|
||||
}]);
|
8
app/portainer/rest/dockerhub.js
Normal file
8
app/portainer/rest/dockerhub.js
Normal 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' }
|
||||
});
|
||||
}]);
|
12
app/portainer/rest/endpoint.js
Normal file
12
app/portainer/rest/endpoint.js
Normal 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'} }
|
||||
});
|
||||
}]);
|
12
app/portainer/rest/registry.js
Normal file
12
app/portainer/rest/registry.js
Normal 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'} }
|
||||
});
|
||||
}]);
|
10
app/portainer/rest/resourceControl.js
Normal file
10
app/portainer/rest/resourceControl.js
Normal 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'} }
|
||||
});
|
||||
}]);
|
10
app/portainer/rest/settings.js
Normal file
10
app/portainer/rest/settings.js
Normal 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' } }
|
||||
});
|
||||
}]);
|
15
app/portainer/rest/stack.js
Normal file
15
app/portainer/rest/stack.js
Normal 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' } }
|
||||
});
|
||||
}]);
|
7
app/portainer/rest/status.js
Normal file
7
app/portainer/rest/status.js
Normal 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' }
|
||||
});
|
||||
}]);
|
12
app/portainer/rest/team.js
Normal file
12
app/portainer/rest/team.js
Normal 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' } }
|
||||
});
|
||||
}]);
|
10
app/portainer/rest/teamMembership.js
Normal file
10
app/portainer/rest/teamMembership.js
Normal 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'} }
|
||||
});
|
||||
}]);
|
6
app/portainer/rest/template.js
Normal file
6
app/portainer/rest/template.js
Normal 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}
|
||||
});
|
||||
}]);
|
16
app/portainer/rest/user.js
Normal file
16
app/portainer/rest/user.js
Normal 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 }
|
||||
});
|
||||
}]);
|
58
app/portainer/services/api/accessService.js
Normal file
58
app/portainer/services/api/accessService.js
Normal 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;
|
||||
}]);
|
26
app/portainer/services/api/dockerhubService.js
Normal file
26
app/portainer/services/api/dockerhubService.js
Normal 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;
|
||||
}]);
|
92
app/portainer/services/api/endpointService.js
Normal file
92
app/portainer/services/api/endpointService.js
Normal 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;
|
||||
}]);
|
92
app/portainer/services/api/registryService.js
Normal file
92
app/portainer/services/api/registryService.js
Normal 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;
|
||||
}]);
|
125
app/portainer/services/api/resourceControlService.js
Normal file
125
app/portainer/services/api/resourceControlService.js
Normal 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;
|
||||
}]);
|
45
app/portainer/services/api/settingsService.js
Normal file
45
app/portainer/services/api/settingsService.js
Normal 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;
|
||||
}]);
|
22
app/portainer/services/api/statusService.js
Normal file
22
app/portainer/services/api/statusService.js
Normal 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;
|
||||
}]);
|
44
app/portainer/services/api/teamMembershipService.js
Normal file
44
app/portainer/services/api/teamMembershipService.js
Normal 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;
|
||||
}]);
|
84
app/portainer/services/api/teamService.js
Normal file
84
app/portainer/services/api/teamService.js
Normal 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;
|
||||
}]);
|
159
app/portainer/services/api/userService.js
Normal file
159
app/portainer/services/api/userService.js
Normal 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;
|
||||
}]);
|
44
app/portainer/services/authentication.js
Normal file
44
app/portainer/services/authentication.js
Normal 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;
|
||||
}
|
||||
};
|
||||
}]);
|
167
app/portainer/services/chartService.js
Normal file
167
app/portainer/services/chartService.js
Normal 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;
|
||||
}]);
|
34
app/portainer/services/codeMirror.js
Normal file
34
app/portainer/services/codeMirror.js
Normal 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;
|
||||
});
|
37
app/portainer/services/datatableService.js
Normal file
37
app/portainer/services/datatableService.js
Normal 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;
|
||||
}]);
|
41
app/portainer/services/endpointProvider.js
Normal file
41
app/portainer/services/endpointProvider.js
Normal 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;
|
||||
}]);
|
35
app/portainer/services/extensionManager.js
Normal file
35
app/portainer/services/extensionManager.js
Normal 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;
|
||||
}]);
|
58
app/portainer/services/fileUpload.js
Normal file
58
app/portainer/services/fileUpload.js
Normal 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;
|
||||
}]);
|
24
app/portainer/services/formValidator.js
Normal file
24
app/portainer/services/formValidator.js
Normal 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;
|
||||
}]);
|
17
app/portainer/services/httpRequestHelper.js
Normal file
17
app/portainer/services/httpRequestHelper.js
Normal 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;
|
||||
}]);
|
75
app/portainer/services/localStorage.js
Normal file
75
app/portainer/services/localStorage.js
Normal 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();
|
||||
}
|
||||
};
|
||||
}]);
|
174
app/portainer/services/modalService.js
Normal file
174
app/portainer/services/modalService.js
Normal 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;
|
||||
}]);
|
38
app/portainer/services/notifications.js
Normal file
38
app/portainer/services/notifications.js
Normal 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;
|
||||
}]);
|
23
app/portainer/services/pagination.js
Normal file
23
app/portainer/services/pagination.js
Normal 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
Loading…
Add table
Add a link
Reference in a new issue