Merge pull request #1116 from jlmitch5/rbac_add_perms

Rbac UI: add permissions modal
This commit is contained in:
jlmitch5 2016-03-10 15:39:03 -05:00
commit 2e232159e0
33 changed files with 1196 additions and 239 deletions

View File

@ -634,6 +634,13 @@ dd {
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 5px rgba(255, 88, 80, 0.6);
}
.form-control.ng-dirty.ng-invalid + .select2 .select2-selection,
.form-control.ng-dirty.ng-invalid + .select2 .select2-selection:focus {
border-color: rgba(255, 88, 80, 0.8) !important;
outline: 0 !important;
box-shadow: none !important;
}
.form-control.ng-dirty.ng-pristine {
border-color: @default-second-border;
box-shadow: none;
@ -2008,15 +2015,33 @@ tr td button i {
box-shadow: none;
}
.form-control + .select2 .select2-selection {
border-color: @default-second-border !important;
background-color: #f6f6f6 !important;
color: @default-data-txt !important;
transition: border-color 0.3s !important;
box-shadow: none !important;
}
.form-control:active, .form-control:focus {
box-shadow: none;
border-color: #167ec4;
}
.form-control:active + .select2 .select2-selection, .form-control:focus + .select2 .select2-selection {
box-shadow: none !important;
border-color: #167ec4 !important;
}
.form-control.ng-dirty.ng-invalid, .form-control.ng-dirty.ng-invalid:focus {
box-shadow: none;
}
.form-control.ng-dirty.ng-invalid + .select2 .select2-selection, .form-control.ng-dirty.ng-invalid:focus + .select2 .select2-selection {
box-shadow: none !important;
}
.error {
opacity: 1;
transition: opacity 0.2s;
@ -2041,3 +2066,7 @@ tr td button i {
.select2-container--disabled {
opacity: .35;
}
body.is-modalOpen {
overflow: hidden;
}

View File

@ -41,6 +41,9 @@ table, tbody {
.List-tableHeader:last-of-type {
border-top-right-radius: 5px;
}
.List-tableHeader--actions {
text-align: right;
}
@ -320,6 +323,11 @@ table, tbody {
height: 34px;
}
.List-searchWidget--compact {
max-width: ~"calc(100% - 91px)";
margin-top: 10px;
}
.List-searchRow {
margin-bottom: 20px;
}

View File

@ -0,0 +1,212 @@
@import "../../shared/branding/colors.default.less";
/** @define AddPermissions */
.AddPermissions-backDrop {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 1041;
opacity: 0.2;
transition: 0.5s opacity;
background: @login-backdrop;
}
.AddPermissions-dialog {
margin: 30px auto;
margin-top: 95px;
}
.AddPermissions-content {
max-width: 750px;
margin: 0 auto;
border: 0;
box-shadow: none;
background-color: @login-bg;
border-radius: 4px;
transition: opacity 0.5s;
z-index: 1042;
position: relative;
opacity: 1;
}
.AddPermissions-header {
padding: 20px;
padding-bottom: 10px;
padding-top: 15px;
}
.AddPermissions-body {
padding: 0px 20px;
}
.AddPermissions-footer {
display: flex;
flex-wrap: wrap-reverse;
align-items: center;
padding: 20px;
padding-bottom: 0px;
padding-top: 20px;
}
.AddPermissions-list .List-searchRow {
height: 0px;
}
.AddPermissions-list .List-searchWidget {
height: 66px;
}
.AddPermissions-list .List-tableHeader:last-child {
border-top-right-radius: 5px;
}
.AddPermissions-list select-all {
display: none;
}
.AddPermissions-title {
margin-top: 5px;
margin-bottom: 20px;
}
.AddPermissions-buttons {
margin-left: auto;
margin-bottom: 20px;
}
.AddPermissions-directions {
margin-top: 10px;
margin-bottom: 20px;
color: #848992;
display: flex;
align-items: center;
}
.AddPermissions-directionNumber {
font-size: 14px;
font-weight: bold;
border-radius: 50%;
background-color: @default-list-header-bg;
padding: 2px 6px;
margin-right: 10px;
}
.AddPermissions-separator {
margin-top: 20px 0px;
width: 100%;
border-bottom: 1px solid @default-second-border;
}
.AddPermissions-roleRow {
display: flex;
margin-bottom: 10px;
align-items: center;
}
.AddPermissions-roleName {
width: 30%;
padding-right: 10px;
display: flex;
align-items: center;
}
.AddPermissions-roleNameVal {
font-size: 14px;
max-width: ~"calc(100% - 46px)";
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.AddPermissions-roleType {
border-radius: 5px;
padding: 0px 6px;
border: 1px solid @default-second-border;
font-size: 10px;
color: @default-interface-txt;
text-transform: uppercase;
background-color: @default-bg;
margin-left: 6px;
}
.AddPermissions-roleSelect {
width: ~"calc(70% - 40px)";
margin-right: 20px;
}
.AddPermissions-roleSelect .Form-dropDown {
height: inherit !important;
}
.AddPermissions-roleRemove {
border-radius: 50%;
padding: 5px 3px;
line-height: 11px;
color: @default-icon;
background-color: @default-tertiary-bg;
border: 0;
}
.AddPermissions-roleRemove:hover {
background-color: @default-err;
color: @default-bg;
}
.AddPermissions-selectHide {
display: none;
}
.AddPermissions .select2-search__field {
text-transform: uppercase;
}
.AddPermissions-keyToggle {
margin-left: auto;
text-transform: uppercase;
padding: 3px 9px;
font-size: 12px;
background-color: @default-bg;
border-radius: 5px;
color: @default-interface-txt;
border: 1px solid @default-second-border;
cursor: pointer;
}
.AddPermissions-keyToggle:hover {
background-color: @default-tertiary-bg;
}
.AddPermissions-keyToggle.is-active {
background-color: @default-link;
border-color: @default-link;
color: @default-bg;
}
.AddPermissions-keyPane {
margin: 20px 0;
border-radius: 5px;
padding: 15px;
padding-bottom: 0px;
border: 1px solid @default-second-border;
color: @default-interface-txt;
}
.AddPermissions-keyRow {
display: flex;
flex-direction: column;
margin-bottom: 15px;
}
.AddPermissions-keyName {
flex: 1 0 auto;
text-transform: uppercase;
font-weight: bold;
padding-bottom: 3px;
}
.AddPermissions-keyDescription {
flex: 1 0 auto;
}

View File

@ -0,0 +1,177 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name controllers.function:Access
* @description
* Controller for handling permissions adding
*/
export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'ProcessErrors', function (rootScope, scope, GetBasePath, Rest, $q, Wait, ProcessErrors) {
var manuallyUpdateChecklists = function(list, id, isSelected) {
var elemScope = angular
.element("#" +
list + "s_table #" + id + ".List-tableRow input")
.scope();
if (elemScope) {
elemScope.isSelected = !!isSelected;
}
};
scope.allSelected = [];
// the object permissions are being added to
scope.object = scope[scope.$parent.list
.iterator + "_obj"];
// array for all possible roles for the object
scope.roles = Object
.keys(scope.object.summary_fields.roles)
.map(function(key) {
return {
value: scope.object.summary_fields
.roles[key].id,
label: scope.object.summary_fields
.roles[key].name };
});
// TODO: get working with api
// array w roles and descriptions for key
scope.roleKey = Object
.keys(scope.object.summary_fields.roles)
.map(function(key) {
return {
name: scope.object.summary_fields
.roles[key].name,
description: scope.object.summary_fields
.roles[key].description };
});
scope.showKeyPane = false;
scope.toggleKeyPane = function() {
scope.showKeyPane = !scope.showKeyPane;
};
// handle form tab changes
scope.toggleFormTabs = function(list) {
scope.usersSelected = (list === 'users');
scope.teamsSelected = !scope.usersSelected;
};
// manually handle selection/deselection of user/team checkboxes
scope.$on("selectedOrDeselected", function(e, val) {
val = val.value;
if (val.isSelected) {
// deselected, so remove from the allSelected list
scope.allSelected = scope.allSelected.filter(function(i) {
// return all but the object who has the id and type
// of the element to deselect
return (!(val.id === i.id && val.type === i.type));
});
} else {
// selected, so add to the allSelected list
scope.allSelected.push({
name: function() {
if (val.type === "user") {
return (val.first_name &&
val.last_name) ?
val.first_name + " " +
val.last_name :
val.username;
} else {
return val .name;
}
},
type: val.type,
roles: [],
id: val.id
});
}
});
// used to handle changes to the itemsSelected scope var on "next page",
// "sorting etc."
scope.$on("itemsSelected", function(e, inList) {
// compile a list of objects that needed to be checked in the lists
scope.updateLists = scope.allSelected.filter(function(inMemory) {
var notInList = true;
inList.forEach(function(val) {
// if the object is part of the allSelected list and is
// selected,
// you don't need to add it updateLists
if (inMemory.id === val.id &&
inMemory.type === val.type) {
notInList = false;
}
});
return notInList;
});
});
// handle changes to the updatedLists by manually selected those values in
// the UI
scope.$watch("updateLists", function(toUpdate) {
(toUpdate || []).forEach(function(obj) {
manuallyUpdateChecklists(obj.type, obj.id, true);
});
delete scope.updateLists;
});
// remove selected user/team
scope.removeObject = function(obj) {
manuallyUpdateChecklists(obj.type, obj.id, false);
scope.allSelected = scope.allSelected.filter(function(i) {
return (!(obj.id === i.id && obj.type === i.type));
});
};
// update post url list
scope.$watch("allSelected", function(val) {
scope.posts = _
.flatten((val || [])
.map(function (owner) {
var url = GetBasePath(owner.type + "s") + owner.id +
"/roles/";
return (owner.roles || [])
.map(function (role) {
return {url: url,
id: role.value};
});
}));
}, true);
// post roles to api
scope.updatePermissions = function() {
Wait('start');
var requests = scope.posts
.map(function(post) {
Rest.setUrl(post.url);
return Rest.post({"id": post.id});
});
$q.all(requests)
.then(function () {
Wait('stop');
rootScope.$broadcast("refreshList", "permission");
scope.closeModal();
}, function (error) {
Wait('stop');
rootScope.$broadcast("refreshList", "permission");
scope.closeModal();
ProcessErrors(null, error.data, error.status, null, {
hdr: 'Error!',
msg: 'Failed to post role(s): POST returned status' +
error.status
});
});
};
}];

View File

@ -0,0 +1,58 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import addPermissionsController from './addPermissions.controller';
/* jshint unused: vars */
export default
[ 'templateUrl',
'Wait',
function(templateUrl, Wait) {
return {
restrict: 'E',
scope: true,
controller: addPermissionsController,
templateUrl: templateUrl('access/addPermissions/addPermissions'),
link: function(scope, element, attrs, ctrl) {
scope.toggleFormTabs('users');
$("body").addClass("is-modalOpen");
$("body").append(element);
Wait('start');
scope.$broadcast("linkLists");
setTimeout(function() {
$('#add-permissions-modal').modal("show");
}, 200);
$('.modal[aria-hidden=false]').each(function () {
if ($(this).attr('id') !== 'add-permissions-modal') {
$(this).modal('hide');
}
});
scope.closeModal = function() {
$("body").removeClass("is-modalOpen");
$('#add-permissions-modal').on('hidden.bs.modal',
function () {
$('.AddPermissions').remove();
});
$('#add-permissions-modal').modal('hide');
};
scope.$on('closePermissionsModal', function() {
scope.closeModal();
});
Wait('stop');
window.scrollTo(0,0);
}
};
}
];

View File

@ -0,0 +1,118 @@
<div id="add-permissions-modal" class="AddPermissions modal fade">
<div class="AddPermissions-backDrop is-loggedOut"></div>
<div class="AddPermissions-dialog">
<div class="AddPermissions-content is-loggedOut">
<div class="AddPermissions-header">
<div class="List-header">
<div class="List-title">
<div class="List-titleText ng-binding">
{{ object.name }}
<div class="List-titleLockup"></div>
Add Permissions
</div>
</div>
<div class="Form-exitHolder">
<button class="Form-exit" ng-click="closeModal()">
<i class="fa fa-times-circle"></i>
</button>
</div>
</div>
</div>
<div class="AddPermissions-body">
<div class="AddPermissions-directions">
<span class="AddPermissions-directionNumber">
1.
</span>
Please select Users / Teams from the lists below.
</div>
<div class="Form-tabHolder">
<div id="users_tab" class="Form-tab"
ng-click="toggleFormTabs('users')"
ng-class="{'is-selected': usersSelected }">
Users
</div>
<div id="teams_tab" class="Form-tab"
ng-click="toggleFormTabs('teams')"
ng-class="{'is-selected': teamsSelected }"
>
Teams
</div>
</div>
<div class="AddPermissions-list" ng-show="usersSelected">
<add-permissions-list type="users">
</add-permissions-list>
</div>
<div class="AddPermissions-list" ng-show="teamsSelected">
<add-permissions-list type="teams">
</add-permissions-list>
</div>
<div class="AddPermissions-separator"
ng-show="allSelected && allSelected.length > 0"></div>
<div class="AddPermissions-directions"
ng-show="allSelected && allSelected.length > 0">
<span class="AddPermissions-directionNumber">
2.
</span>
Please assign roles to the selected users/teams
<div class="AddPermissions-keyToggle"
ng-class="{'is-active': showKeyPane}"
ng-click="toggleKeyPane()">
Key
</div>
</div>
<div class="AddPermissions-keyPane"
ng-show="showKeyPane">
<div class="AddPermissions-keyRow"
ng-repeat="key in roleKey">
<div class="AddPermissions-keyName">
{{ key.name }}
</div>
<div class="AddPermissions-keyDescription">
{{ key.description || "No description provided" }}
</div>
</div>
</div>
<form name="userForm" novalidate>
<ng-form name="userRoleForm">
<div class="AddPermissions-roleRow"
ng-repeat="obj in allSelected">
<div class="AddPermissions-roleName">
<span class="AddPermissions-roleNameVal">
{{ obj.name }}
</span>
<span class="AddPermissions-roleType">
{{ obj.type }}
</span>
</div>
<role-select class="AddPermissions-roleSelect">
</role-select>
<button class="AddPermissions-roleRemove"
ng-click="removeObject(obj)">
<i class="fa fa-times"></i>
</button>
</div>
</ng-form>
</form>
</div>
<div class="AddPermissions-footer">
<div class="buttons Form-buttons AddPermissions-buttons">
<button type="button"
class="btn btn-sm Form-saveButton"
ng-click="updatePermissions()"
ng-disabled="userRoleForm.$invalid || !allSelected || !allSelected.length">
Save
</button>
<button type="button"
class="btn btn-sm Form-cancelButton"
ng-click="closeModal()">
Cancel
</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,58 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/* jshint unused: vars */
export default
['addPermissionsTeamsList', 'addPermissionsUsersList', 'generateList', 'GetBasePath', 'SelectionInit', 'SearchInit',
'PaginateInit', function(addPermissionsTeamsList,
addPermissionsUsersList, generateList,
GetBasePath, SelectionInit, SearchInit, PaginateInit) {
return {
restrict: 'E',
scope: {
},
template: "<div class='addPermissionsList-inner'></div>",
link: function(scope, element, attrs, ctrl) {
scope.$on("linkLists", function(e) {
var generator = generateList,
list = addPermissionsTeamsList,
url = GetBasePath("teams"),
set = "teams",
id = "addPermissionsTeamsList",
mode = "edit";
if (attrs.type === 'users') {
list = addPermissionsUsersList;
url = GetBasePath("users") + "?is_superuser=false";
set = "users";
id = "addPermissionsUsersList";
mode = "edit";
}
scope.id = id;
scope.$watch("selectedItems", function() {
scope.$emit("itemsSelected", scope.selectedItems);
});
element.find(".addPermissionsList-inner")
.attr("id", id);
generator.inject(list, { id: id,
title: false, mode: mode, scope: scope });
SearchInit({ scope: scope, set: set,
list: list, url: url });
PaginateInit({ scope: scope,
list: list, url: url, pageSize: 5 });
scope.search(list.iterator);
});
}
};
}
];

View File

@ -0,0 +1,15 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import addPermissionsListDirective from './addPermissionsList.directive';
import teamsList from './permissionsTeams.list';
import usersList from './permissionsUsers.list';
export default
angular.module('addPermissionsListModule', [])
.directive('addPermissionsList', addPermissionsListDirective)
.factory('addPermissionsTeamsList', teamsList)
.factory('addPermissionsUsersList', usersList);

View File

@ -0,0 +1,27 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default function() {
return {
name: 'teams',
iterator: 'team',
listTitleBadge: false,
multiSelect: true,
multiSelectExtended: true,
index: false,
hover: true,
fields: {
name: {
key: true,
label: 'name'
},
},
};
}

View File

@ -0,0 +1,37 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default function() {
return {
name: 'users',
iterator: 'user',
title: false,
listTitleBadge: false,
multiSelect: true,
multiSelectExtended: true,
index: false,
hover: true,
fields: {
first_name: {
label: 'First Name',
columnClass: 'col-md-3 col-sm-3 hidden-xs'
},
last_name: {
label: 'Last Name',
columnClass: 'col-md-3 col-sm-3 hidden-xs'
},
username: {
key: true,
label: 'Username',
columnClass: 'col-md-3 col-sm-3 col-xs-9'
},
},
};
}

View File

@ -0,0 +1,14 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import addPermissionsDirective from './addPermissions.directive';
import roleSelect from './roleSelect.directive';
import addPermissionsList from './addPermissionsList/main';
export default
angular.module('AddPermissions', [addPermissionsList.name])
.directive('addPermissions', addPermissionsDirective)
.directive('roleSelect', roleSelect);

View File

@ -0,0 +1,25 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/* jshint unused: vars */
export default
[
'CreateSelect2',
function(CreateSelect2) {
return {
restrict: 'E',
scope: false,
template: '<select ng-cloak class="AddPermissions-selectHide roleSelect2 form-control" ng-model="obj.roles" ng-options="role.label for role in roles track by role.value" multiple required></select>',
link: function(scope, element, attrs, ctrl) {
CreateSelect2({
element: '.roleSelect2',
multiple: true,
placeholder: 'Select roles'
});
}
};
}
];

View File

@ -0,0 +1,12 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import roleList from './roleList.directive';
import addPermissions from './addPermissions/main';
export default
angular.module('access', [addPermissions.name])
.directive('roleList', roleList);

View File

@ -0,0 +1,72 @@
/** @define RoleList */
@import "../shared/branding/colors.default.less";
.RoleList {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
}
.RoleList-tagContainer {
display: flex;
max-width: 100%;
}
.RoleList-tag {
border-radius: 5px;
padding: 2px 10px;
margin: 4px 0px;
border: 1px solid @default-second-border;
font-size: 12px;
color: @default-interface-txt;
text-transform: uppercase;
background-color: @default-bg;
margin-right: 5px;
max-width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.RoleList-tag--deletable {
margin-right: 0px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-right: 0;
max-wdith: ~"calc(100% - 23px)";
}
.RoleList-deleteContainer {
border: 1px solid @default-second-border;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
padding: 0 5px;
margin: 4px 0px;
margin-right: 5px;
align-items: center;
display: flex;
cursor: pointer;
}
.RoleList-tagDelete {
font-size: 13px;
color: @default-icon;
}
.RoleList-name {
flex: initial;
max-width: 100%;
}
.RoleList-tag--deletable > .RoleList-name {
max-width: ~"calc(100% - 23px)";
}
.RoleList-deleteContainer:hover, {
border-color: @default-err;
background-color: @default-err;
}
.RoleList-deleteContainer:hover > .RoleList-tagDelete {
color: @default-bg;
}

View File

@ -0,0 +1,44 @@
/* jshint unused: vars */
export default
[ 'templateUrl',
function(templateUrl) {
return {
restrict: 'E',
scope: false,
templateUrl: templateUrl('access/roleList'),
link: function(scope, element, attrs) {
// given a list of roles (things like "project
// auditor") which are pulled from two different
// places in summary fields, and creates a
// concatenated/sorted list
scope.roles = []
.concat(scope.permission.summary_fields
.direct_access.map(function(i) {
return {
name: i.role.name,
roleId: i.role.id,
resourceName: i.role.resource_name,
explicit: true
};
}))
.concat(scope.permission.summary_fields
.indirect_access.map(function(i) {
return {
name: i.role.name,
roleId: i.role.id,
explicit: false
};
}))
.sort(function(a, b) {
if (a.name
.toLowerCase() > b.name
.toLowerCase()) {
return 1;
} else {
return -1;
}
});
}
};
}
];

View File

@ -0,0 +1,13 @@
<div class="RoleList-tagContainer"
ng-repeat="role in roles">
<div class="RoleList-tag"
ng-class="{'RoleList-tag--deletable': role.explicit}">
<span class="RoleList-name">{{ role.name }}</span>
</div>
<div class="RoleList-deleteContainer"
ng-if="role.explicit"
ng-click="deletePermission(permission.id, role.roleId, permission.username, role.name, role.resourceName)">
<i ng-if="role.explicit"
class="fa fa-times RoleList-tagDelete"></i>
</div>
</div>

View File

@ -31,6 +31,7 @@ import permissions from './permissions/main';
import managementJobs from './management-jobs/main';
import jobDetail from './job-detail/main';
import notifications from './notifications/main';
import access from './access/main';
// modules
import about from './about/main';
@ -101,6 +102,7 @@ var tower = angular.module('Tower', [
jobDetail.name,
notifications.name,
standardOut.name,
access.name,
'templates',
'Utilities',
'OrganizationFormDefinition',
@ -884,16 +886,42 @@ var tower = angular.module('Tower', [
}]);
}])
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', '$state', 'CheckLicense',
'$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket',
'LoadConfig', 'Store', 'ShowSocketHelp', 'pendoService',
function (
$q, $compile, $cookieStore, $rootScope, $log, $state, CheckLicense,
$location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
LoadConfig, Store, ShowSocketHelp, pendoService)
{
.run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket',
'LoadConfig', 'Store', 'ShowSocketHelp', 'AboutAnsibleHelp', 'pendoService', 'Prompt', 'Rest', 'Wait', 'ProcessErrors', '$state', 'GetBasePath',
function ($q, $compile, $cookieStore, $rootScope, $log, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, Socket,
LoadConfig, Store, ShowSocketHelp, AboutAnsibleHelp, pendoService, Prompt, Rest, Wait, ProcessErrors, $state, GetBasePath) {
var sock;
$rootScope.addPermission = function (scope) {
$compile("<add-permissions class='AddPermissions'></add-permissions>")(scope);
}
$rootScope.deletePermission = function (user, role, userName,
roleName, resourceName) {
var action = function () {
$('#prompt-modal').modal('hide');
Wait('start');
var url = GetBasePath("users") + user + "/roles/";
Rest.setUrl(url);
Rest.post({"disassociate": true, "id": role})
.success(function () {
Wait('stop');
$rootScope.$broadcast("refreshList", "permission");
})
.error(function (data, status) {
ProcessErrors($rootScope, data, status, null, { hdr: 'Error!',
msg: 'Could not disacssociate user from role. Call to ' + url + ' failed. DELETE returned status: ' + status });
});
};
Prompt({
hdr: 'Remove Role from ' + resourceName,
body: '<div class="Prompt-bodyQuery">Confirm the removal of the <span class="Prompt-emphasis">' + roleName + '</span> role associated with ' + userName + '.</div>',
action: action,
actionText: 'REMOVE'
});
};
function activateTab() {
// Make the correct tab active
var base = $location.path().replace(/^\//, '').split('/')[0];
@ -1027,6 +1055,7 @@ var tower = angular.module('Tower', [
$rootScope.$on("$stateChangeStart", function (event, next, nextParams, prev) {
$rootScope.$broadcast("closePermissionsModal");
// this line removes the query params attached to a route
if(prev && prev.$$route &&
prev.$$route.name === 'systemTracking'){

View File

@ -649,7 +649,6 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
}
}
relatedSets = form.relatedSets(data.related);

View File

@ -210,36 +210,40 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log,
$scope.$emit("RefreshTeamsList");
// return a promise from the options request with the permission type choices (including adhoc) as a param
var permissionsChoice = fieldChoices({
scope: $scope,
url: 'api/v1/' + base + '/' + id + '/permissions/',
field: 'permission_type'
});
// var permissionsChoice = fieldChoices({
// scope: $scope,
// url: 'api/v1/' + base + '/' + id + '/permissions/',
// field: 'permission_type'
// });
// manipulate the choices from the options request to be set on
// scope and be usable by the list form
permissionsChoice.then(function (choices) {
choices =
fieldLabels({
choices: choices
});
_.map(choices, function(n, key) {
$scope.permission_label[key] = n;
});
});
// // manipulate the choices from the options request to be set on
// // scope and be usable by the list form
// permissionsChoice.then(function (choices) {
// choices =
// fieldLabels({
// choices: choices
// });
// _.map(choices, function(n, key) {
// $scope.permission_label[key] = n;
// });
// });
// manipulate the choices from the options request to be usable
// by the search option for permission_type, you can't inject the
// list until this is done!
permissionsChoice.then(function (choices) {
form.related.permissions.fields.permission_type.searchOptions =
permissionsSearchSelect({
choices: choices
});
// permissionsChoice.then(function (choices) {
// form.related.permissions.fields.permission_type.searchOptions =
// permissionsSearchSelect({
// choices: choices
// });
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();
$scope.$emit('loadTeam');
});
// });
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();
$scope.$emit('loadTeam');
$scope.team_id = id;

View File

@ -240,37 +240,38 @@ export function UsersEdit($scope, $rootScope, $compile, $location, $log,
$scope.$emit("RefreshUsersList");
// return a promise from the options request with the permission type choices (including adhoc) as a param
var permissionsChoice = fieldChoices({
scope: $scope,
url: 'api/v1/' + base + '/' + id + '/permissions/',
field: 'permission_type'
});
// manipulate the choices from the options request to be set on
// scope and be usable by the list form
permissionsChoice.then(function (choices) {
choices =
fieldLabels({
choices: choices
});
_.map(choices, function(n, key) {
$scope.permission_label[key] = n;
});
});
// // return a promise from the options request with the permission type choices (including adhoc) as a param
// var permissionsChoice = fieldChoices({
// scope: $scope,
// url: 'api/v1/' + base + '/' + id + '/permissions/',
// field: 'permission_type'
// });
//
// // manipulate the choices from the options request to be set on
// // scope and be usable by the list form
// permissionsChoice.then(function (choices) {
// choices =
// fieldLabels({
// choices: choices
// });
// _.map(choices, function(n, key) {
// $scope.permission_label[key] = n;
// });
// });
// manipulate the choices from the options request to be usable
// by the search option for permission_type, you can't inject the
// list until this is done!
permissionsChoice.then(function (choices) {
form.related.permissions.fields.permission_type.searchOptions =
permissionsSearchSelect({
choices: choices
});
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();
$scope.$emit("loadForm");
});
// permissionsChoice.then(function (choices) {
// form.related.permissions.fields.permission_type.searchOptions =
// permissionsSearchSelect({
// choices: choices
// });
// });
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();
$scope.$emit("loadForm");
if ($scope.removeFormReady) {
$scope.removeFormReady();

View File

@ -7,7 +7,6 @@
import sanitizeFilter from './shared/xss-sanitizer.filter';
import capitalizeFilter from './shared/capitalize.filter';
import longDateFilter from './shared/long-date.filter';
export {
sanitizeFilter,
capitalizeFilter,

View File

@ -278,83 +278,38 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
}
}
},
schedules: {
permissions: {
type: 'collection',
title: 'Schedules',
iterator: 'schedule',
title: 'Permissions',
iterator: 'permission',
index: false,
open: false,
searchType: 'select',
actions: {
refresh: {
mode: 'all',
awToolTip: "Refresh the page",
ngClick: "refreshSchedules()",
actionClass: 'btn List-buttonDefault',
buttonContent: 'REFRESH',
ngHide: 'scheduleLoading == false && schedule_active_search == false && schedule_total_rows < 1'
},
add: {
mode: 'all',
ngClick: 'addSchedule()',
awToolTip: 'Add a new schedule',
ngClick: "addPermission",
label: 'Add',
awToolTip: 'Add a permission',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
}
},
fields: {
name: {
username: {
key: true,
label: 'Name',
ngClick: "editSchedule(schedule.id)",
columnClass: "col-md-3 col-sm-3 col-xs-3"
label: 'User',
linkBase: 'users',
class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4'
},
dtstart: {
label: 'First Run',
filter: "longDate",
searchable: false,
columnClass: "col-md-2 col-sm-3 hidden-xs"
},
next_run: {
label: 'Next Run',
filter: "longDate",
searchable: false,
columnClass: "col-md-2 col-sm-3 col-xs-3"
},
dtend: {
label: 'Final Run',
filter: "longDate",
searchable: false,
columnClass: "col-md-2 col-sm-3 hidden-xs"
}
},
fieldActions: {
"play": {
mode: "all",
ngClick: "toggleSchedule($event, schedule.id)",
awToolTip: "{{ schedule.play_tip }}",
dataTipWatch: "schedule.play_tip",
iconClass: "{{ 'fa icon-schedule-enabled-' + schedule.enabled }}",
dataPlacement: "top"
},
edit: {
label: 'Edit',
ngClick: "editSchedule(schedule.id)",
icon: 'icon-edit',
awToolTip: 'Edit schedule',
dataPlacement: 'top'
},
"delete": {
label: 'Delete',
ngClick: "deleteSchedule(schedule.id)",
icon: 'icon-trash',
awToolTip: 'Delete schedule',
dataPlacement: 'top'
role: {
label: 'Role',
type: 'role',
noSort: true,
class: 'col-lg-9 col-md-9 col-sm-9 col-xs-8'
}
}
}
},
relatedSets: function(urls) {
@ -363,9 +318,9 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
iterator: 'organization',
url: urls.organizations
},
schedules: {
iterator: 'schedule',
url: urls.schedules
permissions: {
iterator: 'permission',
url: urls.resource_access_list
}
};
}

View File

@ -158,69 +158,69 @@ export default
}
},
permissions: {
type: 'collection',
title: 'Permissions',
iterator: 'permission',
open: false,
index: false,
actions: {
add: {
ngClick: "add('permissions')",
label: 'Add',
awToolTip: 'Add a permission for this user',
ngShow: 'PermissionAddAllowed',
actionClass: 'btn List-buttonSubmit',
buttonContent: '&#43; ADD'
}
},
fields: {
name: {
key: true,
label: 'Name',
ngClick: "edit('permissions', permission.id, permission.name)"
},
inventory: {
label: 'Inventory',
sourceModel: 'inventory',
sourceField: 'name',
ngBind: 'permission.summary_fields.inventory.name'
},
project: {
label: 'Project',
sourceModel: 'project',
sourceField: 'name',
ngBind: 'permission.summary_fields.project.name'
},
permission_type: {
label: 'Permission',
ngBind: 'getPermissionText()',
searchType: 'select'
}
},
fieldActions: {
edit: {
label: 'Edit',
ngClick: "edit('permissions', permission.id, permission.name)",
icon: 'icon-edit',
awToolTip: 'Edit the permission',
'class': 'btn btn-default'
},
"delete": {
label: 'Delete',
ngClick: "delete('permissions', permission.id, permission.name, 'permission')",
icon: 'icon-trash',
"class": 'btn-danger',
awToolTip: 'Delete the permission',
ngShow: 'PermissionAddAllowed'
}
}
},
// permissions: {
// type: 'collection',
// title: 'Permissions',
// iterator: 'permission',
// open: false,
// index: false,
//
// actions: {
// add: {
// ngClick: "add('permissions')",
// label: 'Add',
// awToolTip: 'Add a permission for this user',
// ngShow: 'PermissionAddAllowed',
// actionClass: 'btn List-buttonSubmit',
// buttonContent: '&#43; ADD'
// }
// },
//
// fields: {
// name: {
// key: true,
// label: 'Name',
// ngClick: "edit('permissions', permission.id, permission.name)"
// },
// inventory: {
// label: 'Inventory',
// sourceModel: 'inventory',
// sourceField: 'name',
// ngBind: 'permission.summary_fields.inventory.name'
// },
// project: {
// label: 'Project',
// sourceModel: 'project',
// sourceField: 'name',
// ngBind: 'permission.summary_fields.project.name'
// },
// permission_type: {
// label: 'Permission',
// ngBind: 'getPermissionText()',
// searchType: 'select'
// }
// },
//
// fieldActions: {
// edit: {
// label: 'Edit',
// ngClick: "edit('permissions', permission.id, permission.name)",
// icon: 'icon-edit',
// awToolTip: 'Edit the permission',
// 'class': 'btn btn-default'
// },
//
// "delete": {
// label: 'Delete',
// ngClick: "delete('permissions', permission.id, permission.name, 'permission')",
// icon: 'icon-trash',
// "class": 'btn-danger',
// awToolTip: 'Delete the permission',
// ngShow: 'PermissionAddAllowed'
// }
// }
//
// },
admin_of_organizations: { // Assumes a plural name (e.g. things)
type: 'collection',

View File

@ -32,7 +32,7 @@ export default
// Which page are we on?
if (Empty(next) && previous) {
// no next page, but there is a previous page
scope[iterator + '_page'] = scope[iterator + '_num_pages'];
scope[iterator + '_page'] = /page=\d+/.test(previous) ? parseInt(previous.match(/page=(\d+)/)[1]) + 1 : 2;
} else if (next && Empty(previous)) {
// next page available, but no previous page
scope[iterator + '_page'] = 1;

View File

@ -230,10 +230,15 @@ export default
url += (url.match(/\/$/)) ? '?' : '&';
url += scope[iterator + 'SearchParams'];
url += (scope[iterator + '_page_size']) ? '&page_size=' + scope[iterator + '_page_size'] : "";
scope[iterator + '_active_search'] = true;
RefreshRelated({ scope: scope, set: set, iterator: iterator, url: url });
};
scope.$on("refreshList", function(e, iterator) {
scope.search(iterator);
});
scope.sort = function (iterator, fld) {
var sort_order, icon, direction, set;

View File

@ -85,7 +85,9 @@ export default
}
Store('sessionTime', x);
$rootScope.lastUser = $cookieStore.get('current_user').id;
if ($cookieStore.get('current_user')) {
$rootScope.lastUser = $cookieStore.get('current_user').id;
}
$cookieStore.remove('token_expires');
$cookieStore.remove('current_user');
$cookieStore.remove('token');

View File

@ -136,6 +136,10 @@
.MainMenu-itemText--username {
padding-left: 13px;
margin-top: -4px;
max-width: 85px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.MainMenu-itemImage {

View File

@ -614,7 +614,8 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter'])
var element = params.element,
options = params.opts,
multiple = (params.multiple!==undefined) ? params.multiple : true;
multiple = (params.multiple!==undefined) ? params.multiple : true,
placeholder = params.placeholder;
$.fn.select2.amd.require([
'select2/utils',
@ -632,6 +633,7 @@ angular.module('Utilities', ['RestServices', 'Utilities', 'sanitizeFilter'])
}, Dropdown);
$(element).select2({
placeholder: placeholder,
multiple: multiple,
containerCssClass: 'Form-dropDown',
width: '100%',

View File

@ -1548,7 +1548,9 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += "<div class=\"buttons Form-buttons\" ";
html += "id=\"" + this.form.name + "_controls\" ";
if (options.mode === 'edit' && this.form.tabs) {
html += "ng-show=\"" + this.form.name + "Selected\"; "
}
html += ">\n";
if (this.form.horizontal) {
@ -1723,32 +1725,52 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += "<tr class=\"List-tableHeaderRow\">\n";
html += (collection.index === undefined || collection.index !== false) ? "<th class=\"col-xs-1\">#</th>\n" : "";
for (fld in collection.fields) {
html += "<th class=\"List-tableHeader list-header\" id=\"" + collection.iterator + '-' + fld + "-header\" " +
"ng-click=\"sort('" + collection.iterator + "', '" + fld + "')\">" +
collection.fields[fld].label;
html += " <i class=\"";
if (collection.fields[fld].key) {
if (collection.fields[fld].desc) {
html += "fa fa-sort-down";
} else {
html += "fa fa-sort-up";
}
html += "<th class=\"List-tableHeader list-header ";
html += (collection.fields[fld].class) ? collection.fields[fld].class : "";
html += "\" id=\"" + collection.iterator + '-' + fld + "-header\" ";
if (!collection.fields[fld].noSort) {
html += "ng-click=\"sort('" + collection.iterator + "', '" + fld + "')\">"
} else {
html += "fa fa-sort";
html += ">";
}
html += "\"></i></a></th>\n";
html += collection.fields[fld].label;
if (!collection.fields[fld].noSort) {
html += " <i class=\"";
if (collection.fields[fld].key) {
if (collection.fields[fld].desc) {
html += "fa fa-sort-down";
} else {
html += "fa fa-sort-up";
}
} else {
html += "fa fa-sort";
}
html += "\"></i>"
}
html += "</a></th>\n";
}
if (collection.fieldActions) {
html += "<th class=\"List-tableHeader List-tableHeader--actions\">Actions</th>\n";
}
html += "<th class=\"List-tableHeader\">Actions</th>\n";
html += "</tr>\n";
html += "</thead>";
html += "<tbody>\n";
html += "<tr class=\"List-tableHeaderRow\" ng-repeat=\"" + collection.iterator + " in " + itm + "\" ";
html += "<tr class=\"List-tableRow\" ng-repeat=\"" + collection.iterator + " in " + itm + "\" ";
html += "ng-class-odd=\"'List-tableRow--oddRow'\" ";
html += "ng-class-even=\"'List-tableRow--evenRow'\" ";
html += "id=\"{{ " + collection.iterator + ".id }}\">\n";
if (collection.index === undefined || collection.index !== false) {
html += "<td class=\"List-tableCell\">{{ $index + ((" + collection.iterator + "_page - 1) * " +
html += "<td class=\"List-tableCell";
html += (collection.fields[fld].class) ? collection.fields[fld].class : "";
html += "\">{{ $index + ((" + collection.iterator + "_page - 1) * " +
collection.iterator + "_page_size) + 1 }}.</td>\n";
}
cnt = 1;
@ -1765,31 +1787,33 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
}
// Row level actions
html += "<td class=\"List-tableCell List-actionButtonCell actions\">";
for (act in collection.fieldActions) {
fAction = collection.fieldActions[act];
html += "<button id=\"" + ((fAction.id) ? fAction.id : act + "-action") + "\" ";
html += (fAction.href) ? "href=\"" + fAction.href + "\" " : "";
html += (fAction.ngClick) ? this.attr(fAction, 'ngClick') : "";
html += (fAction.ngHref) ? this.attr(fAction, 'ngHref') : "";
html += (fAction.ngShow) ? this.attr(fAction, 'ngShow') : "";
html += " class=\"List-actionButton ";
html += (act === 'delete') ? "List-actionButton--delete" : "";
html += "\"";
html += ">";
if (fAction.iconClass) {
html += "<i class=\"" + fAction.iconClass + "\"></i>";
} else {
html += SelectIcon({
action: act
});
if (collection.fieldActions) {
html += "<td class=\"List-tableCell List-actionButtonCell actions\">";
for (act in collection.fieldActions) {
fAction = collection.fieldActions[act];
html += "<button id=\"" + ((fAction.id) ? fAction.id : act + "-action") + "\" ";
html += (fAction.href) ? "href=\"" + fAction.href + "\" " : "";
html += (fAction.ngClick) ? this.attr(fAction, 'ngClick') : "";
html += (fAction.ngHref) ? this.attr(fAction, 'ngHref') : "";
html += (fAction.ngShow) ? this.attr(fAction, 'ngShow') : "";
html += " class=\"List-actionButton ";
html += (act === 'delete') ? "List-actionButton--delete" : "";
html += "\"";
html += ">";
if (fAction.iconClass) {
html += "<i class=\"" + fAction.iconClass + "\"></i>";
} else {
html += SelectIcon({
action: act
});
}
// html += SelectIcon({ action: act });
//html += (fAction.label) ? "<span class=\"list-action-label\"> " + fAction.label + "</span>": "";
html += "</button>";
}
// html += SelectIcon({ action: act });
//html += (fAction.label) ? "<span class=\"list-action-label\"> " + fAction.label + "</span>": "";
html += "</button>";
html += "</td>";
html += "</tr>\n";
}
html += "</td>";
html += "</tr>\n";
// Message for loading
html += "<tr ng-show=\"" + collection.iterator + "Loading == true\">\n";

View File

@ -449,6 +449,8 @@ angular.module('GeneratorHelpers', [systemStatus.name])
if (field.type !== undefined && field.type === 'DropDown') {
html = DropDown(params);
} else if (field.type === 'role') {
html += "<td class=\"List-tableCell\"><role-list class=\"RoleList\"></role-list></td>";
} else if (field.type === 'badgeCount') {
html = BadgeCount(params);
} else if (field.type === 'badgeOnly') {
@ -520,7 +522,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
list: list,
field: field,
fld: fld,
base: base
base: field.linkBase || base
}) + ' ';
});
}
@ -532,7 +534,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
list: list,
field: field,
fld: fld,
base: base
base: field.linkBase || base
});
}
}
@ -633,6 +635,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
var iterator = params.iterator,
form = params.template,
size = params.size,
mini = params.mini,
includeSize = (params.includeSize === undefined) ? true : params.includeSize,
ngShow = (params.ngShow) ? params.ngShow : false,
i, html = '',
@ -666,6 +669,7 @@ angular.module('GeneratorHelpers', [systemStatus.name])
if (includeSize) {
html += "<div class=\"List-searchWidget ";
html += (mini) ? "List-searchWidget--compact " : "";
html += (size) ? size : "col-lg-4 col-md-8 col-sm-12 col-xs-12";
html += "\" id=\"search-widget-container" + modifier + "\">\n";
}

View File

@ -416,6 +416,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
function buildTable() {
var extraClasses = list['class'];
var multiSelect = list.multiSelect ? 'multi-select-list' : null;
var multiSelectExtended = list.multiSelectExtended ? 'true' : 'false';
if (options.mode === 'summary') {
extraClasses += ' table-summary';
@ -425,7 +426,8 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
.attr('id', list.name + '_table')
.addClass('List-table')
.addClass(extraClasses)
.attr('multi-select-list', multiSelect);
.attr('multi-select-list', multiSelect)
.attr('is-extended', multiSelectExtended);
}
@ -460,7 +462,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
}
if (list.multiSelect) {
innerTable += '<td class="col-xs-1 select-column List-tableCell"><select-list-item item=\"' + list.iterator + '\"></select-list-item></td>';
innerTable += '<td class="col-xs-1 select-column List- List-staticColumn--smallStatus"><select-list-item item=\"' + list.iterator + '\"></select-list-item></td>';
}
// Change layout if a lookup list, place radio buttons before labels
@ -609,7 +611,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
function buildSelectAll() {
return $('<th>')
.addClass('col-xs-1 select-column List-tableHeader')
.addClass('col-xs-1 select-column List-tableHeader List-staticColumn--smallStatus')
.append(
$('<select-all>')
.attr('selections-empty', 'selectedItems.length === 0')
@ -665,10 +667,9 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
}
}
if (options.mode === 'select') {
html += "<th class=\"List-tableHeader col-lg-1 col-md-1 col-sm-2 col-xs-2\">Select</th>";
}
else if (options.mode === 'edit' && list.fieldActions) {
html += "<th class=\"List-tableHeader actions-column";
html += "<th class=\"List-tableHeader col-lg-1 col-md-1 col-sm-2 col-xs-2\">Select</th>";
} else if (options.mode === 'edit' && list.fieldActions) {
html += "<th class=\"List-tableHeader List-tableHeader--actions actions-column";
html += (list.fieldActions && list.fieldActions.columnClass) ? " " + list.fieldActions.columnClass : "";
html += "\">";
html += (list.fieldActions.label === undefined || list.fieldActions.label) ? "Actions" : "";

View File

@ -30,7 +30,7 @@ export default
item: '=item'
},
require: '^multiSelectList',
template: '<input type="checkbox" data-multi-select-list-item ng-model="isSelected">',
template: '<input type="checkbox" data-multi-select-list-item ng-model="isSelected" ng-change="userInteractionSelect()">',
link: function(scope, element, attrs, multiSelectList) {
scope.isSelected = false;
@ -52,6 +52,10 @@ export default
multiSelectList.deregisterItem(scope.decoratedItem);
});
scope.userInteractionSelect = function() {
scope.$emit("selectedOrDeselected", scope.decoratedItem);
}
}
};
}];

View File

@ -8,3 +8,8 @@
.Prompt-bodyTarget {
color: @default-data-txt;
}
.Prompt-emphasis {
font-weight: bold;
text-transform: uppercase;
}