From 9ea0803b457a20ec31fb8039297e9cd3f89594e8 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 29 Feb 2016 12:05:23 -0500 Subject: [PATCH 01/12] added role list and deletion to projects object --- awx/ui/client/legacy-styles/lists.less | 2 +- awx/ui/client/src/access/main.js | 12 +++ awx/ui/client/src/access/roleList.block.less | 71 ++++++++++++++ .../client/src/access/roleList.directive.js | 44 +++++++++ .../client/src/access/roleList.partial.html | 13 +++ awx/ui/client/src/app.js | 40 ++++++-- awx/ui/client/src/controllers/Projects.js | 1 - awx/ui/client/src/filters.js | 1 - awx/ui/client/src/forms/Projects.js | 85 ++++------------ .../client/src/helpers/PaginationHelpers.js | 2 +- awx/ui/client/src/helpers/related-search.js | 5 + .../authentication.service.js | 4 +- .../client/src/main-menu/main-menu.block.less | 4 + awx/ui/client/src/shared/form-generator.js | 98 ++++++++++++------- awx/ui/client/src/shared/generator-helpers.js | 6 +- .../list-generator/list-generator.factory.js | 7 +- awx/ui/client/src/shared/prompt/prompt.less | 5 + 17 files changed, 278 insertions(+), 122 deletions(-) create mode 100644 awx/ui/client/src/access/main.js create mode 100644 awx/ui/client/src/access/roleList.block.less create mode 100644 awx/ui/client/src/access/roleList.directive.js create mode 100644 awx/ui/client/src/access/roleList.partial.html diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index ba6adba673..0929fd4a28 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -39,7 +39,7 @@ table, tbody { border-top-left-radius: 5px; } -.List-tableHeader:last-of-type { +.List-tableHeader--actions { border-top-right-radius: 5px; text-align: right; } diff --git a/awx/ui/client/src/access/main.js b/awx/ui/client/src/access/main.js new file mode 100644 index 0000000000..5b7063938b --- /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', []) + .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..2e006791cf --- /dev/null +++ b/awx/ui/client/src/access/roleList.block.less @@ -0,0 +1,71 @@ +/** @define RoleList */ + +.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 #e1e1e1; + font-size: 12px; + color: #848992; + text-transform: uppercase; + background-color: #fff; + 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 #e1e1e1; + 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: #b7b7b7; +} + +.RoleList-name { + flex: initial; + max-width: 100%; +} + +.RoleList-tag--deletable > .RoleList-name { + max-width: ~"calc(100% - 23px)"; +} + +.RoleList-deleteContainer:hover, { + border-color: #ff5850; + background-color: #ff5850; +} + +.RoleList-deleteContainer:hover > .RoleList-tagDelete { + color: #fff; +} 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..974e0e885e 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,38 @@ 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', + function ($q, $compile, $cookieStore, $rootScope, $log, CheckLicense, $location, Authorization, LoadBasePaths, Timer, ClearScope, Socket, + LoadConfig, Store, ShowSocketHelp, AboutAnsibleHelp, pendoService, Prompt, Rest, Wait, ProcessErrors, $state) { var sock; + $rootScope.deletePermission = function (user, role, userName, + roleName, resourceName) { + var action = function () { + $('#prompt-modal').modal('hide'); + Wait('start'); + var url = "/api/v1/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: '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]; 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/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/helpers/PaginationHelpers.js b/awx/ui/client/src/helpers/PaginationHelpers.js index 2b131c2dc0..4bbc46cd03 100644 --- a/awx/ui/client/src/helpers/PaginationHelpers.js +++ b/awx/ui/client/src/helpers/PaginationHelpers.js @@ -134,7 +134,7 @@ export default } else if (mode === 'lookup') { scope[iterator + '_page_size'] = 5; } else { - scope[iterator + '_page_size'] = 20; + scope[iterator + '_page_size'] = 2; } scope.getPage = function (page, set, iterator) { 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/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 693a090f68..c7187d7106 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -1723,32 +1723,52 @@ 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 +1785,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..44bff438d9 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 }); } } 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..de6aed9081 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 @@ -665,10 +665,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/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; +} From 2f1ee901e4035cf1cb4600b9e932e990e392ec19 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 29 Feb 2016 13:12:13 -0500 Subject: [PATCH 02/12] fixes to pr --- awx/ui/client/src/app.js | 8 ++++---- awx/ui/client/src/helpers/PaginationHelpers.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 974e0e885e..892c5f66b6 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -887,9 +887,9 @@ var tower = angular.module('Tower', [ }]) .run(['$q', '$compile', '$cookieStore', '$rootScope', '$log', 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'ClearScope', 'Socket', - 'LoadConfig', 'Store', 'ShowSocketHelp', 'AboutAnsibleHelp', 'pendoService', 'Prompt', 'Rest', 'Wait', 'ProcessErrors', '$state', + '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) { + LoadConfig, Store, ShowSocketHelp, AboutAnsibleHelp, pendoService, Prompt, Rest, Wait, ProcessErrors, $state, GetBasePath) { var sock; $rootScope.deletePermission = function (user, role, userName, @@ -897,7 +897,7 @@ var tower = angular.module('Tower', [ var action = function () { $('#prompt-modal').modal('hide'); Wait('start'); - var url = "/api/v1/users/" + user + "/roles/"; + var url = GetBasePath("users") + user + "/roles/"; Rest.setUrl(url); Rest.post({"disassociate": true, "id": role}) .success(function () { @@ -906,7 +906,7 @@ var tower = angular.module('Tower', [ }) .error(function (data, status) { ProcessErrors($rootScope, data, status, null, { hdr: 'Error!', - msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + msg: 'Could not disacssociate user from role. Call to ' + url + ' failed. DELETE returned status: ' + status }); }); }; diff --git a/awx/ui/client/src/helpers/PaginationHelpers.js b/awx/ui/client/src/helpers/PaginationHelpers.js index 4bbc46cd03..2b131c2dc0 100644 --- a/awx/ui/client/src/helpers/PaginationHelpers.js +++ b/awx/ui/client/src/helpers/PaginationHelpers.js @@ -134,7 +134,7 @@ export default } else if (mode === 'lookup') { scope[iterator + '_page_size'] = 5; } else { - scope[iterator + '_page_size'] = 2; + scope[iterator + '_page_size'] = 20; } scope.getPage = function (page, set, iterator) { From 6b37054621ef3ae0ab82db37374de3a502f2efd8 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 4 Mar 2016 00:39:03 -0500 Subject: [PATCH 03/12] working commit rbac add permissions --- awx/ui/client/legacy-styles/ansible-ui.less | 25 ++ awx/ui/client/legacy-styles/lists.less | 5 + .../addPermissions/addPermissions.block.less | 144 ++++++++++++ .../addPermissions.controller.js | 217 ++++++++++++++++++ .../addPermissions.directive.js | 51 ++++ .../addPermissions.partial.html | 105 +++++++++ .../client/src/access/addPermissions/main.js | 15 ++ .../addPermissions/roleSelect.directive.js | 25 ++ .../src/access/addPermissions/teams/main.js | 13 ++ .../teams/permissionsTeams.directive.js | 44 ++++ .../teams/permissionsTeams.list.js | 27 +++ .../src/access/addPermissions/users/main.js | 13 ++ .../users/permissionsUsers.directive.js | 44 ++++ .../users/permissionsUsers.list.js | 37 +++ awx/ui/client/src/access/main.js | 2 +- awx/ui/client/src/app.js | 4 + awx/ui/client/src/controllers/Teams.js | 48 ++-- awx/ui/client/src/controllers/Users.js | 55 ++--- awx/ui/client/src/forms/Users.js | 126 +++++----- .../client/src/helpers/PaginationHelpers.js | 2 +- awx/ui/client/src/shared/Utilities.js | 4 +- awx/ui/client/src/shared/form-generator.js | 4 +- awx/ui/client/src/shared/generator-helpers.js | 2 + .../list-generator/list-generator.factory.js | 8 +- .../select-list-item.directive.js | 6 +- 25 files changed, 906 insertions(+), 120 deletions(-) create mode 100644 awx/ui/client/src/access/addPermissions/addPermissions.block.less create mode 100644 awx/ui/client/src/access/addPermissions/addPermissions.controller.js create mode 100644 awx/ui/client/src/access/addPermissions/addPermissions.directive.js create mode 100644 awx/ui/client/src/access/addPermissions/addPermissions.partial.html create mode 100644 awx/ui/client/src/access/addPermissions/main.js create mode 100644 awx/ui/client/src/access/addPermissions/roleSelect.directive.js create mode 100644 awx/ui/client/src/access/addPermissions/teams/main.js create mode 100644 awx/ui/client/src/access/addPermissions/teams/permissionsTeams.directive.js create mode 100644 awx/ui/client/src/access/addPermissions/teams/permissionsTeams.list.js create mode 100644 awx/ui/client/src/access/addPermissions/users/main.js create mode 100644 awx/ui/client/src/access/addPermissions/users/permissionsUsers.directive.js create mode 100644 awx/ui/client/src/access/addPermissions/users/permissionsUsers.list.js diff --git a/awx/ui/client/legacy-styles/ansible-ui.less b/awx/ui/client/legacy-styles/ansible-ui.less index 717ab7bd30..742e9a24c1 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; diff --git a/awx/ui/client/legacy-styles/lists.less b/awx/ui/client/legacy-styles/lists.less index 0929fd4a28..2e6f5e7b0c 100644 --- a/awx/ui/client/legacy-styles/lists.less +++ b/awx/ui/client/legacy-styles/lists.less @@ -320,6 +320,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..06e666e248 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/addPermissions.block.less @@ -0,0 +1,144 @@ +@import "../../shared/branding/colors.default.less"; + +/** @define AddPermissions */ +.AddPermissions { + position: absolute; + top: 0; + width: 100%; + height: 100%; +} + +.AddPermissions-content { + max-width: 750px !important; +} + +.AddPermissions-header { + padding: 20px; + padding-bottom: 10px; + padding-top: 15px; +} + +.AddPermissions-body { + padding-top: 0px !important; + max-height: 70vh; + overflow: scroll; +} + +.AddPermissions-footer { + padding-top: 20px !important; +} + +.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; +} + +.AddPermissions-directionNumber { + font-size: 14px; + font-weight: bold; + border-radius: 50%; + background-color: #ebebeb; + padding-left: 6px; + padding-right: 1px; + padding-bottom: 3px; + margin-right: 10px; +} + +.AddPermissions-separator { + margin-top: 20px; + margin-bottom: 20px; + width: 100%; + border-bottom: 1px solid #e1e1e1; +} + +.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 #e1e1e1; + font-size: 10px; + color: #848992; + text-transform: uppercase; + background-color: #fff; + 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: 3px; + line-height: 11px; + padding-left: 5px; + padding-right: 5px; + color: #b7b7b7; + background-color: #fafafa; + border: 0; +} + +.AddPermissions-roleRemove:hover { + background-color: #ff5850; + color: #fff; +} + +.AddPermissions-selectHide { + display: none; +} + +.AddPermissions .select2-search__field { + text-transform: uppercase; +} 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..24f152d1d7 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/addPermissions.controller.js @@ -0,0 +1,217 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/** + * @ngdoc function + * @name controllers.function:Authentication + * @description + * Controller for handling /#/login and /#/logout routes. + * + * Tower (app.js) verifies the user is authenticated and that the user session is not expired. If either condition is not true, + * the user is redirected to /#/login and the Authentication controller. + * + * Methods for checking the session state are found in [js/shared/AuthService.js](/static/docs/api/shared.function:AuthService), which is referenced here as Authorization. + * + * #Login Modal Dialog + * + * The modal dialog prompting for username and password is found in templates/ui/index.html. + *``` + * + * + *``` + * HTML for the login form is generated, compiled and injected into
by the controller. This is done to associate the form with the controller's scope. Because + *
is outside of the ng-view container, it gets associated with $rootScope by default. In the controller we create a new scope using $rootScope.$new() and associate + * that with the login form. Doing this each time the controller is instantiated insures the form is clean and not pre-populated with a prior user's username and password. + * + * Just before the release of 2.0 a bug was discovered where clicking logout and then immediately clicking login without providing a username and password would successfully log + * the user back into Tower. Implementing the above approach fixed this, forcing a new username/password to be entered each time the login dialog appears. + * + * #Login Workflow + * + * When the the login button is clicked, the following occurs: + * + * - Call Authorization.retrieveToken(username, password) - sends a POST request to /api/v1/authtoken to get a new token value. + * - Call Authorization.setToken(token, expires) to store the token and exipration time in a session cookie. + * - Start the expiration timer by calling the init() method of [js/shared/Timer.js](/static/docs/api/shared.function:Timer) + * - Get user informaton by calling Authorization.getUser() - sends a GET request to /api/v1/me + * - Store user information in the session cookie by calling Authorization.setUser(). + * - Get the Tower license by calling Authorization.getLicense() - sends a GET request to /api/vi/config + * - Stores the license object in local storage by calling Authorization.setLicense(). This adds the Tower version and a tested flag to the license object. The tested flag is initially set to false. + * + * Note that there is a session timer kept on the server side as well as the client side. Each time an API request is made, Tower (in app.js) calls + * Timer.isExpired(). This verifies the UI does not think the session is expired, and if not, moves the expiration time into the future. The number of + * seconds between API calls before a session is considered expired is set in config.js as session_timeout. + * + * @Usage + * This is usage information. + */ + +export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', function (rootScope, scope, GetBasePath, Rest, $q) { + 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 }; + }); + + // handle form tabs + scope.toggleFormTabs = function(list) { + scope.usersSelected = (list === 'users'); + scope.teamsSelected = !scope.usersSelected; + }; + + // TODO: manually handle selection/deselection + // of user/team checkboxes + scope.$on("selectedOrDeselected", function(e, val) { + val = val.value; + if (val.isSelected) { + scope.allSelected = scope.allSelected.filter(function(i) { + return (!(val.id === i.id && val.type === i.type)); + }); + } else { + var name; + + if (val.type === "user") { + name = (val.first_name && + val.last_name) ? + val.first_name + " " + + val.last_name : + val.username; + } else { + name = val.name; + } + + scope.allSelected.push({ + name: name, + type: val.type, + roles: [], + id: val.id + }); + } + }); + + scope.$on("itemsSelected", function(e, inList) { + scope.updateLists = scope.allSelected.filter(function(inMemory) { + var notInList = true; + inList.forEach(function(val) { + if (inMemory.id === val.id && + inMemory.type === val.type) { + notInList = false; + } + }); + return notInList; + }); + }); + + scope.$watch("updateLists", function(toUpdate) { + (toUpdate || []).forEach(function(obj) { + var elemScope = angular + .element("#" + + obj.type + "s_table #" + obj.id + + ".List-tableRow input") + .scope() + if (elemScope) { + elemScope.isSelected = true; + } + }); + + delete scope.updateLists; + }); + + // create array of users/teams + // scope.$watchGroup(['selectedUsers', 'selectedTeams'], + // function(val) { + // scope.allSelected = (val[0] || []) + // .map(function(i) { + // var roles = i.roles || []; + // var name = (i.first_name && + // i.last_name) ? + // i.first_name + " " + + // i.last_name : + // i.username; + // + // return { + // name: name, + // type: "user", + // roles: roles, + // id: i.id + // }; + // }).concat((val[1] || []) + // .map(function(i) { + // var roles = i.roles || []; + // + // return { + // name: i.name, + // type: "team", + // roles: roles, + // id: i.id + // }; + // })); + // }); + + // remove selected user/team + scope.removeObject = function(obj) { + var elemScope = angular + .element("#" + + obj.type + "s_table #" + obj.id + ".List-tableRow input") + .scope() + if (elemScope) { + elemScope.isSelected = 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() { + var requests = scope.posts + .map(function(post) { + Rest.setUrl(post.url); + return Rest.post({"id": post.id}); + }); + + $q.all(requests) + .then(function (responses) { + rootScope.$broadcast("refreshList", "permission"); + scope.closeModal(); + }, function (error) { + // TODO: request(s) errored out. Call process errors + }); + }; +}]; 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..7a631f19a7 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/addPermissions.directive.js @@ -0,0 +1,51 @@ +/************************************************* + * 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").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() { + $('#add-permissions-modal').on('hidden.bs.modal', + function () { + $('.AddPermissions').remove(); + }); + $('#add-permissions-modal').modal('hide'); + }; + + 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..68eb34cffa --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/addPermissions.partial.html @@ -0,0 +1,105 @@ + 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..001aa08b59 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/main.js @@ -0,0 +1,15 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import addPermissionsDirective from './addPermissions.directive'; +import roleSelect from './roleSelect.directive'; +import teamsPermissions from './teams/main'; +import usersPermissions from './users/main'; + +export default + angular.module('AddPermissions', [teamsPermissions.name, usersPermissions.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/addPermissions/teams/main.js b/awx/ui/client/src/access/addPermissions/teams/main.js new file mode 100644 index 0000000000..ccba15fe86 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/teams/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import teamsDirective from './permissionsTeams.directive'; +import teamsList from './permissionsTeams.list'; + +export default + angular.module('PermissionsTeams', []) + .directive('addPermissionsTeams', teamsDirective) + .factory('addPermissionsTeamsList', teamsList); diff --git a/awx/ui/client/src/access/addPermissions/teams/permissionsTeams.directive.js b/awx/ui/client/src/access/addPermissions/teams/permissionsTeams.directive.js new file mode 100644 index 0000000000..158aeb1a23 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/teams/permissionsTeams.directive.js @@ -0,0 +1,44 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/* jshint unused: vars */ +export default + ['addPermissionsTeamsList', 'generateList', 'GetBasePath', 'SelectionInit', 'SearchInit', + 'PaginateInit', function(addPermissionsTeamsList, generateList, + GetBasePath, SelectionInit, SearchInit, PaginateInit) { + return { + restrict: 'E', + scope: { + }, + template: "
", + link: function(scope, element, attrs, ctrl) { + scope.$on("linkLists", function() { + var generator = generateList, + list = addPermissionsTeamsList, + url = GetBasePath("teams"), + set = "teams", + id = "addPermissionsTeamsList", + mode = "edit"; + + scope.$watch("selectedItems", function() { + scope.$emit("itemsSelected", scope.selectedItems); + }); + + 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/teams/permissionsTeams.list.js b/awx/ui/client/src/access/addPermissions/teams/permissionsTeams.list.js new file mode 100644 index 0000000000..dc30bfbaf5 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/teams/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/users/main.js b/awx/ui/client/src/access/addPermissions/users/main.js new file mode 100644 index 0000000000..e565e6c410 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/users/main.js @@ -0,0 +1,13 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +import usersDirective from './permissionsUsers.directive'; +import usersList from './permissionsUsers.list'; + +export default + angular.module('PermissionsUsers', []) + .directive('addPermissionsUsers', usersDirective) + .factory('addPermissionsUsersList', usersList); diff --git a/awx/ui/client/src/access/addPermissions/users/permissionsUsers.directive.js b/awx/ui/client/src/access/addPermissions/users/permissionsUsers.directive.js new file mode 100644 index 0000000000..aa36c9c7eb --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/users/permissionsUsers.directive.js @@ -0,0 +1,44 @@ +/************************************************* + * Copyright (c) 2015 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +/* jshint unused: vars */ +export default + ['addPermissionsUsersList', 'generateList', 'GetBasePath', 'SelectionInit', 'SearchInit', + 'PaginateInit', function(addPermissionsUsersList, generateList, + GetBasePath, SelectionInit, SearchInit, PaginateInit) { + return { + restrict: 'E', + scope: { + }, + template: "
", + link: function(scope, element, attrs, ctrl) { + scope.$on("linkLists", function() { + var generator = generateList, + list = addPermissionsUsersList, + url = GetBasePath("users") + "?is_superuser=false", + set = "users", + id = "addPermissionsUsersList", + mode = "edit"; + + scope.$watch("selectedItems", function() { + scope.$emit("itemsSelected", scope.selectedItems); + }); + + 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/users/permissionsUsers.list.js b/awx/ui/client/src/access/addPermissions/users/permissionsUsers.list.js new file mode 100644 index 0000000000..ced865e944 --- /dev/null +++ b/awx/ui/client/src/access/addPermissions/users/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/main.js b/awx/ui/client/src/access/main.js index 5b7063938b..084fe5ef87 100644 --- a/awx/ui/client/src/access/main.js +++ b/awx/ui/client/src/access/main.js @@ -8,5 +8,5 @@ import roleList from './roleList.directive'; import addPermissions from './addPermissions/main'; export default - angular.module('access', []) + angular.module('access', [addPermissions.name]) .directive('roleList', roleList); diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 892c5f66b6..14e955f423 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -892,6 +892,10 @@ var tower = angular.module('Tower', [ 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 () { 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/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/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 c7187d7106..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"; } 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 de6aed9081..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') 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); + } + } }; }]; From 433ba95addeef37eb6861b153b82283b5641ec50 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 4 Mar 2016 11:14:01 -0500 Subject: [PATCH 04/12] working commit of rbac add permissions flow --- .../access/addPermissions/addPermissions.controller.js | 10 ++++++++++ .../access/addPermissions/addPermissions.partial.html | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.controller.js b/awx/ui/client/src/access/addPermissions/addPermissions.controller.js index 24f152d1d7..39909e1170 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissions.controller.js +++ b/awx/ui/client/src/access/addPermissions/addPermissions.controller.js @@ -72,6 +72,16 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', function (r .roles[key].name }; }); + 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 }; + }); + // handle form tabs scope.toggleFormTabs = function(list) { scope.usersSelected = (list === 'users'); diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.partial.html b/awx/ui/client/src/access/addPermissions/addPermissions.partial.html index 68eb34cffa..e9ef9b7d07 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissions.partial.html +++ b/awx/ui/client/src/access/addPermissions/addPermissions.partial.html @@ -19,7 +19,6 @@
-
1. From 56364617fd47cc68a4a70eb21f2fd4cc4045665b Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 7 Mar 2016 11:19:43 -0500 Subject: [PATCH 05/12] clean up of ui rbac code --- .../addPermissions/addPermissions.block.less | 63 ++++++-- .../addPermissions.controller.js | 152 +++++------------- .../addPermissions.directive.js | 2 +- .../addPermissions.partial.html | 22 ++- .../addPermissionsList.directive.js} | 22 ++- .../addPermissions/addPermissionsList/main.js | 15 ++ .../permissionsTeams.list.js | 0 .../permissionsUsers.list.js | 0 .../client/src/access/addPermissions/main.js | 5 +- .../src/access/addPermissions/teams/main.js | 13 -- .../src/access/addPermissions/users/main.js | 13 -- .../users/permissionsUsers.directive.js | 44 ----- awx/ui/client/src/access/roleList.block.less | 17 +- 13 files changed, 145 insertions(+), 223 deletions(-) rename awx/ui/client/src/access/addPermissions/{teams/permissionsTeams.directive.js => addPermissionsList/addPermissionsList.directive.js} (61%) create mode 100644 awx/ui/client/src/access/addPermissions/addPermissionsList/main.js rename awx/ui/client/src/access/addPermissions/{teams => addPermissionsList}/permissionsTeams.list.js (100%) rename awx/ui/client/src/access/addPermissions/{users => addPermissionsList}/permissionsUsers.list.js (100%) delete mode 100644 awx/ui/client/src/access/addPermissions/teams/main.js delete mode 100644 awx/ui/client/src/access/addPermissions/users/main.js delete mode 100644 awx/ui/client/src/access/addPermissions/users/permissionsUsers.directive.js diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.block.less b/awx/ui/client/src/access/addPermissions/addPermissions.block.less index 06e666e248..3a49aef10e 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissions.block.less +++ b/awx/ui/client/src/access/addPermissions/addPermissions.block.less @@ -1,15 +1,39 @@ @import "../../shared/branding/colors.default.less"; /** @define AddPermissions */ -.AddPermissions { - position: absolute; + +.AddPermissions-backDrop { + width: 100vw; + height: 100vh; + position: fixed; top: 0; - width: 100%; - height: 100%; + left: 0; + z-index: 1041; + opacity: 0; + transition: 0.5s opacity; + background: @login-backdrop; + opacity: 0.2; +} + +.AddPermissions-dialog { + margin: 30px auto; + margin-top: 95px; } .AddPermissions-content { - max-width: 750px !important; + max-width: 750px; + margin-left: auto; + margin-right: auto; + border: 0; + box-shadow: none; + background-color: @login-bg; + border-radius: 4px; + opacity: 0; + transition: opacity 0.5s; + z-index: 1042; + position: relative; + opacity: 1; + transition: opacity 0.5s; } .AddPermissions-header { @@ -19,13 +43,20 @@ } .AddPermissions-body { - padding-top: 0px !important; + padding-left: 20px; + padding-right: 20px; + padding-top: 0px; max-height: 70vh; overflow: scroll; } .AddPermissions-footer { - padding-top: 20px !important; + display: flex; + flex-wrap: wrap-reverse; + align-items: center; + padding: 20px; + padding-bottom: 0px; + padding-top: 20px; } .AddPermissions-list .List-searchRow { @@ -64,7 +95,7 @@ font-size: 14px; font-weight: bold; border-radius: 50%; - background-color: #ebebeb; + background-color: @default-list-header-bg; padding-left: 6px; padding-right: 1px; padding-bottom: 3px; @@ -75,7 +106,7 @@ margin-top: 20px; margin-bottom: 20px; width: 100%; - border-bottom: 1px solid #e1e1e1; + border-bottom: 1px solid @default-second-border; } .AddPermissions-roleRow { @@ -102,11 +133,11 @@ .AddPermissions-roleType { border-radius: 5px; padding: 0px 6px; - border: 1px solid #e1e1e1; + border: 1px solid @default-second-border; font-size: 10px; - color: #848992; + color: @default-interface-txt; text-transform: uppercase; - background-color: #fff; + background-color: @default-bg; margin-left: 6px; } @@ -125,14 +156,14 @@ line-height: 11px; padding-left: 5px; padding-right: 5px; - color: #b7b7b7; - background-color: #fafafa; + color: @default-icon; + background-color: @default-tertiary-bg; border: 0; } .AddPermissions-roleRemove:hover { - background-color: #ff5850; - color: #fff; + background-color: @default-err; + color: @default-bg; } .AddPermissions-selectHide { diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.controller.js b/awx/ui/client/src/access/addPermissions/addPermissions.controller.js index 39909e1170..4e60995dec 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissions.controller.js +++ b/awx/ui/client/src/access/addPermissions/addPermissions.controller.js @@ -6,55 +6,22 @@ /** * @ngdoc function - * @name controllers.function:Authentication + * @name controllers.function:Access * @description - * Controller for handling /#/login and /#/logout routes. - * - * Tower (app.js) verifies the user is authenticated and that the user session is not expired. If either condition is not true, - * the user is redirected to /#/login and the Authentication controller. - * - * Methods for checking the session state are found in [js/shared/AuthService.js](/static/docs/api/shared.function:AuthService), which is referenced here as Authorization. - * - * #Login Modal Dialog - * - * The modal dialog prompting for username and password is found in templates/ui/index.html. - *``` - * - * - *``` - * HTML for the login form is generated, compiled and injected into
by the controller. This is done to associate the form with the controller's scope. Because - *
is outside of the ng-view container, it gets associated with $rootScope by default. In the controller we create a new scope using $rootScope.$new() and associate - * that with the login form. Doing this each time the controller is instantiated insures the form is clean and not pre-populated with a prior user's username and password. - * - * Just before the release of 2.0 a bug was discovered where clicking logout and then immediately clicking login without providing a username and password would successfully log - * the user back into Tower. Implementing the above approach fixed this, forcing a new username/password to be entered each time the login dialog appears. - * - * #Login Workflow - * - * When the the login button is clicked, the following occurs: - * - * - Call Authorization.retrieveToken(username, password) - sends a POST request to /api/v1/authtoken to get a new token value. - * - Call Authorization.setToken(token, expires) to store the token and exipration time in a session cookie. - * - Start the expiration timer by calling the init() method of [js/shared/Timer.js](/static/docs/api/shared.function:Timer) - * - Get user informaton by calling Authorization.getUser() - sends a GET request to /api/v1/me - * - Store user information in the session cookie by calling Authorization.setUser(). - * - Get the Tower license by calling Authorization.getLicense() - sends a GET request to /api/vi/config - * - Stores the license object in local storage by calling Authorization.setLicense(). This adds the Tower version and a tested flag to the license object. The tested flag is initially set to false. - * - * Note that there is a session timer kept on the server side as well as the client side. Each time an API request is made, Tower (in app.js) calls - * Timer.isExpired(). This verifies the UI does not think the session is expired, and if not, moves the expiration time into the future. The number of - * seconds between API calls before a session is considered expired is set in config.js as session_timeout. - * - * @Usage - * This is usage information. + * Controller for handling permissions adding */ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', function (rootScope, scope, GetBasePath, Rest, $q) { + 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 @@ -72,6 +39,8 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', function (r .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) { @@ -82,35 +51,36 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', function (r .roles[key].description }; }); - // handle form tabs + // handle form tab changes scope.toggleFormTabs = function(list) { scope.usersSelected = (list === 'users'); scope.teamsSelected = !scope.usersSelected; }; - // TODO: manually handle selection/deselection - // of user/team checkboxes + // 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 { - var name; - - if (val.type === "user") { - name = (val.first_name && - val.last_name) ? - val.first_name + " " + - val.last_name : - val.username; - } else { - name = val.name; - } - + // selected, so add to the allSelected list scope.allSelected.push({ - name: name, + 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 @@ -118,10 +88,16 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', function (r } }); + // 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; @@ -131,61 +107,19 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', function (r }); }); + // handle changes to the updatedLists by manually selected those values in + // the UI scope.$watch("updateLists", function(toUpdate) { (toUpdate || []).forEach(function(obj) { - var elemScope = angular - .element("#" + - obj.type + "s_table #" + obj.id + - ".List-tableRow input") - .scope() - if (elemScope) { - elemScope.isSelected = true; - } + manuallyUpdateChecklists(obj.type, obj.id, true); }); delete scope.updateLists; }); - // create array of users/teams - // scope.$watchGroup(['selectedUsers', 'selectedTeams'], - // function(val) { - // scope.allSelected = (val[0] || []) - // .map(function(i) { - // var roles = i.roles || []; - // var name = (i.first_name && - // i.last_name) ? - // i.first_name + " " + - // i.last_name : - // i.username; - // - // return { - // name: name, - // type: "user", - // roles: roles, - // id: i.id - // }; - // }).concat((val[1] || []) - // .map(function(i) { - // var roles = i.roles || []; - // - // return { - // name: i.name, - // type: "team", - // roles: roles, - // id: i.id - // }; - // })); - // }); - // remove selected user/team scope.removeObject = function(obj) { - var elemScope = angular - .element("#" + - obj.type + "s_table #" + obj.id + ".List-tableRow input") - .scope() - if (elemScope) { - elemScope.isSelected = false; - } + manuallyUpdateChecklists(obj.type, obj.id, false); scope.allSelected = scope.allSelected.filter(function(i) { return (!(obj.id === i.id && obj.type === i.type)); @@ -197,7 +131,7 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', function (r scope.posts = _ .flatten((val || []) .map(function (owner) { - var url = GetBasePath(owner.type + "s") + "/" + owner.id + + var url = GetBasePath(owner.type + "s") + owner.id + "/roles/"; return (owner.roles || []) @@ -217,7 +151,7 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', function (r }); $q.all(requests) - .then(function (responses) { + .then(function () { rootScope.$broadcast("refreshList", "permission"); scope.closeModal(); }, function (error) { diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.directive.js b/awx/ui/client/src/access/addPermissions/addPermissions.directive.js index 7a631f19a7..c96f0e3701 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissions.directive.js +++ b/awx/ui/client/src/access/addPermissions/addPermissions.directive.js @@ -17,7 +17,7 @@ export default templateUrl: templateUrl('access/addPermissions/addPermissions'), link: function(scope, element, attrs, ctrl) { scope.toggleFormTabs('users'); - + $("body").append(element); Wait('start'); diff --git a/awx/ui/client/src/access/addPermissions/addPermissions.partial.html b/awx/ui/client/src/access/addPermissions/addPermissions.partial.html index e9ef9b7d07..dc824b7a3e 100644 --- a/awx/ui/client/src/access/addPermissions/addPermissions.partial.html +++ b/awx/ui/client/src/access/addPermissions/addPermissions.partial.html @@ -1,7 +1,7 @@ -