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 += "";
html += "\n";
- 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 += "";
- }
- else if (options.mode === 'edit' && list.fieldActions) {
- html += "";
+ } else if (options.mode === 'edit' && list.fieldActions) {
+ html += "