diff --git a/awx/ui/client/legacy-styles/ansible-ui.less b/awx/ui/client/legacy-styles/ansible-ui.less index 717ab7bd30..8b5be38541 100644 --- a/awx/ui/client/legacy-styles/ansible-ui.less +++ b/awx/ui/client/legacy-styles/ansible-ui.less @@ -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; +} diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index ba6adba673..3c257e525c 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -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; } diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.block.less b/awx/ui/client/src/access/addPermissions/addPermissions.block.less new file mode 100644 index 0000000000..87e4e0ee2b --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/addPermissions.block.less @@ -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; +} diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.controller.js b/awx/ui/client/src/access/addPermissions/addPermissions.controller.js new file mode 100644 index 0000000000..b95f851f07 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/addPermissions.controller.js @@ -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 + }); + }); + }; +}]; diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.directive.js b/awx/ui/client/src/access/addPermissions/addPermissions.directive.js new file mode 100644 index 0000000000..a9d21dfff8 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/addPermissions.directive.js @@ -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); + } + }; + } + ]; diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.partial.html b/awx/ui/client/src/access/addPermissions/addPermissions.partial.html new file mode 100644 index 0000000000..dd81f1a823 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/addPermissions.partial.html @@ -0,0 +1,118 @@ + diff --git a/awx/ui/client/src/access/addPermissions/addPermissionsList/addPermissionsList.directive.js b/awx/ui/client/src/access/addPermissions/addPermissionsList/addPermissionsList.directive.js new file mode 100644 index 0000000000..6342ec0b8d --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/addPermissionsList/addPermissionsList.directive.js @@ -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: "
", + 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); + }); + } + }; + } + ]; diff --git a/awx/ui/client/src/access/addPermissions/addPermissionsList/main.js b/awx/ui/client/src/access/addPermissions/addPermissionsList/main.js new file mode 100644 index 0000000000..c523ca2032 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/addPermissionsList/main.js @@ -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); diff --git a/awx/ui/client/src/access/addPermissions/addPermissionsList/permissionsTeams.list.js b/awx/ui/client/src/access/addPermissions/addPermissionsList/permissionsTeams.list.js new file mode 100644 index 0000000000..dc30bfbaf5 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/addPermissionsList/permissionsTeams.list.js @@ -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' + }, + }, + + }; +} diff --git a/awx/ui/client/src/access/addPermissions/addPermissionsList/permissionsUsers.list.js b/awx/ui/client/src/access/addPermissions/addPermissionsList/permissionsUsers.list.js new file mode 100644 index 0000000000..ced865e944 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/addPermissionsList/permissionsUsers.list.js @@ -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' + }, + }, + + }; +} diff --git a/awx/ui/client/src/access/addPermissions/main.js b/awx/ui/client/src/access/addPermissions/main.js new file mode 100644 index 0000000000..ca627908de --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/main.js @@ -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); diff --git a/awx/ui/client/src/access/addPermissions/roleSelect.directive.js b/awx/ui/client/src/access/addPermissions/roleSelect.directive.js new file mode 100644 index 0000000000..c11dbe0e67 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/roleSelect.directive.js @@ -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: '', + link: function(scope, element, attrs, ctrl) { + CreateSelect2({ + element: '.roleSelect2', + multiple: true, + placeholder: 'Select roles' + }); + } + }; + } + ]; diff --git a/awx/ui/client/src/access/main.js b/awx/ui/client/src/access/main.js new file mode 100644 index 0000000000..084fe5ef87 --- /dev/null +++ b/awx/ui/client/src/access/main.js @@ -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); diff --git a/awx/ui/client/src/access/roleList.block.less b/awx/ui/client/src/access/roleList.block.less new file mode 100644 index 0000000000..8bc4dd38de --- /dev/null +++ b/awx/ui/client/src/access/roleList.block.less @@ -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; +} diff --git a/awx/ui/client/src/access/roleList.directive.js b/awx/ui/client/src/access/roleList.directive.js new file mode 100644 index 0000000000..376a00f085 --- /dev/null +++ b/awx/ui/client/src/access/roleList.directive.js @@ -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; + } + }); + } + }; + } + ]; diff --git a/awx/ui/client/src/access/roleList.partial.html b/awx/ui/client/src/access/roleList.partial.html new file mode 100644 index 0000000000..bc49322d45 --- /dev/null +++ b/awx/ui/client/src/access/roleList.partial.html @@ -0,0 +1,13 @@ +
+
+ {{ role.name }} +
+
+ +
+
diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index a23e803de2..0743630ffc 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -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("")(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: '
Confirm the removal of the ' + roleName + ' role associated with ' + userName + '.
', + 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'){ diff --git a/awx/ui/client/src/controllers/Projects.js b/awx/ui/client/src/controllers/Projects.js index 911ebcf356..c75cc5193f 100644 --- a/awx/ui/client/src/controllers/Projects.js +++ b/awx/ui/client/src/controllers/Projects.js @@ -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); diff --git a/awx/ui/client/src/controllers/Teams.js b/awx/ui/client/src/controllers/Teams.js index 7d71bd8ee2..0bb7e9e2d4 100644 --- a/awx/ui/client/src/controllers/Teams.js +++ b/awx/ui/client/src/controllers/Teams.js @@ -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; diff --git a/awx/ui/client/src/controllers/Users.js b/awx/ui/client/src/controllers/Users.js index f6b6b74a13..c17d6456f5 100644 --- a/awx/ui/client/src/controllers/Users.js +++ b/awx/ui/client/src/controllers/Users.js @@ -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(); diff --git a/awx/ui/client/src/filters.js b/awx/ui/client/src/filters.js index da0123cad7..e6549bb779 100644 --- a/awx/ui/client/src/filters.js +++ b/awx/ui/client/src/filters.js @@ -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, diff --git a/awx/ui/client/src/forms/Projects.js b/awx/ui/client/src/forms/Projects.js index dbf04c457f..aa39d4bd7d 100644 --- a/awx/ui/client/src/forms/Projects.js +++ b/awx/ui/client/src/forms/Projects.js @@ -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: '+ 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 } }; } diff --git a/awx/ui/client/src/forms/Users.js b/awx/ui/client/src/forms/Users.js index 5d87cac2db..820f86a8cf 100644 --- a/awx/ui/client/src/forms/Users.js +++ b/awx/ui/client/src/forms/Users.js @@ -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: '+ 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: '+ 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', diff --git a/awx/ui/client/src/helpers/PaginationHelpers.js b/awx/ui/client/src/helpers/PaginationHelpers.js index 2b131c2dc0..2fd9d57bf2 100644 --- a/awx/ui/client/src/helpers/PaginationHelpers.js +++ b/awx/ui/client/src/helpers/PaginationHelpers.js @@ -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; diff --git a/awx/ui/client/src/helpers/related-search.js b/awx/ui/client/src/helpers/related-search.js index cad094d349..04bb72f071 100644 --- a/awx/ui/client/src/helpers/related-search.js +++ b/awx/ui/client/src/helpers/related-search.js @@ -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; diff --git a/awx/ui/client/src/login/authenticationServices/authentication.service.js b/awx/ui/client/src/login/authenticationServices/authentication.service.js index d648bdd1eb..912ada41a8 100644 --- a/awx/ui/client/src/login/authenticationServices/authentication.service.js +++ b/awx/ui/client/src/login/authenticationServices/authentication.service.js @@ -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'); diff --git a/awx/ui/client/src/main-menu/main-menu.block.less b/awx/ui/client/src/main-menu/main-menu.block.less index 5e9b342c2c..d64b5de3d3 100644 --- a/awx/ui/client/src/main-menu/main-menu.block.less +++ b/awx/ui/client/src/main-menu/main-menu.block.less @@ -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 { diff --git a/awx/ui/client/src/shared/Utilities.js b/awx/ui/client/src/shared/Utilities.js index 434526cd7d..7aeae22b6b 100644 --- a/awx/ui/client/src/shared/Utilities.js +++ b/awx/ui/client/src/shared/Utilities.js @@ -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%', diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 693a090f68..8525ea7997 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -1548,7 +1548,9 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += "
\n"; html += (collection.index === undefined || collection.index !== false) ? "#\n" : ""; for (fld in collection.fields) { - html += "" + - collection.fields[fld].label; - html += " " } else { - html += "fa fa-sort"; + html += ">"; } - html += "\">\n"; + + + html += collection.fields[fld].label; + + if (!collection.fields[fld].noSort) { + html += " " + } + + html += "\n"; + } + if (collection.fieldActions) { + html += "Actions\n"; } - html += "Actions\n"; html += "\n"; html += ""; html += "\n"; - html += "\n"; if (collection.index === undefined || collection.index !== false) { - html += "{{ $index + ((" + collection.iterator + "_page - 1) * " + + html += "{{ $index + ((" + collection.iterator + "_page - 1) * " + collection.iterator + "_page_size) + 1 }}.\n"; } cnt = 1; @@ -1765,31 +1787,33 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat } // Row level actions - html += ""; - for (act in collection.fieldActions) { - fAction = collection.fieldActions[act]; - html += ""; } - // html += SelectIcon({ action: act }); - //html += (fAction.label) ? " " + fAction.label + "": ""; - html += ""; + html += ""; + html += "\n"; } - html += ""; - html += "\n"; // Message for loading html += "\n"; diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js index 1fc8880e63..ae841fffd4 100644 --- a/awx/ui/client/src/shared/generator-helpers.js +++ b/awx/ui/client/src/shared/generator-helpers.js @@ -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 += ""; } 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 += "
\n"; } diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index 5071dfee54..2205f84390 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -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 += ''; + innerTable += ''; } // 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 $('') - .addClass('col-xs-1 select-column List-tableHeader') + .addClass('col-xs-1 select-column List-tableHeader List-staticColumn--smallStatus') .append( $('') .attr('selections-empty', 'selectedItems.length === 0') @@ -665,10 +667,9 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate } } if (options.mode === 'select') { - html += "Select"; - } - else if (options.mode === 'edit' && list.fieldActions) { - html += "Select"; + } else if (options.mode === 'edit' && list.fieldActions) { + html += ""; html += (list.fieldActions.label === undefined || list.fieldActions.label) ? "Actions" : ""; diff --git a/awx/ui/client/src/shared/multi-select-list/select-list-item.directive.js b/awx/ui/client/src/shared/multi-select-list/select-list-item.directive.js index f701288090..fb90d045b8 100644 --- a/awx/ui/client/src/shared/multi-select-list/select-list-item.directive.js +++ b/awx/ui/client/src/shared/multi-select-list/select-list-item.directive.js @@ -30,7 +30,7 @@ export default item: '=item' }, require: '^multiSelectList', - template: '', + template: '', 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); + } + } }; }]; diff --git a/awx/ui/client/src/shared/prompt/prompt.less b/awx/ui/client/src/shared/prompt/prompt.less index e0e5ef0c30..5c491b64e3 100644 --- a/awx/ui/client/src/shared/prompt/prompt.less +++ b/awx/ui/client/src/shared/prompt/prompt.less @@ -8,3 +8,8 @@ .Prompt-bodyTarget { color: @default-data-txt; } + +.Prompt-emphasis { + font-weight: bold; + text-transform: uppercase; +}