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 @@
+
+
+
+
+
+
+
+
+ 1.
+
+ Please select Users / Teams from the lists below.
+
+
+
+
+
+
+
+
+
+
+
+ 2.
+
+ Please assign roles to the selected users/teams
+
+ Key
+
+
+
+
+
+ {{ key.name }}
+
+
+ {{ key.description || "No description provided" }}
+
+
+
+
+
+
+
+
+
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 += "