${html}
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 4e2fe0609a..3dbb9514c4 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
@@ -583,7 +583,8 @@ export default ['$compile', 'Attr', 'Icon',
},
wrapPanel: function(html){
- return `
${html}
`;
+ return `
+
${html}
`;
},
insertFormView: function(){
diff --git a/awx/ui/client/src/users/edit/users-edit.controller.js b/awx/ui/client/src/users/edit/users-edit.controller.js
index 289f2d5556..0accb1bdcf 100644
--- a/awx/ui/client/src/users/edit/users-edit.controller.js
+++ b/awx/ui/client/src/users/edit/users-edit.controller.js
@@ -30,6 +30,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'UserForm', 'Rest',
init();
function init() {
+ $scope.isCurrentlyLoggedInUser = (parseInt(id) === $rootScope.current_user.id);
$scope.hidePagination = false;
$scope.hideSmartSearch = false;
$scope.user_type_options = user_type_options;
diff --git a/awx/ui/client/src/users/main.js b/awx/ui/client/src/users/main.js
index aa883c7001..95765ffa3a 100644
--- a/awx/ui/client/src/users/main.js
+++ b/awx/ui/client/src/users/main.js
@@ -9,6 +9,11 @@ import UsersAdd from './add/users-add.controller';
import UsersEdit from './edit/users-edit.controller';
import UserForm from './users.form';
import UserList from './users.list';
+import UserTokensListRoute from './users-tokens-list.route';
+import UserTokensAddRoute from './users-tokens-add.route';
+import UserTokensAddApplicationRoute from './users-tokens-add-application.route';
+import TokensStrings from './tokens.strings';
+
import { N_ } from '../i18n';
export default
@@ -18,16 +23,15 @@ angular.module('Users', [])
.controller('UsersEdit', UsersEdit)
.factory('UserForm', UserForm)
.factory('UserList', UserList)
- .config(['$stateProvider', 'stateDefinitionsProvider',
- function($stateProvider, stateDefinitionsProvider) {
- let stateDefinitions = stateDefinitionsProvider.$get();
+ .service('TokensStrings', TokensStrings)
- // lazily generate a tree of substates which will replace this node in ui-router's stateRegistry
- // see: stateDefinition.factory for usage documentation
- $stateProvider.state({
- name: 'users.**',
- url: '/users',
- lazyLoad: () => stateDefinitions.generateTree({
+ .config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider',
+ function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) {
+ let stateDefinitions = stateDefinitionsProvider.$get();
+ let stateExtender = $stateExtenderProvider.$get();
+
+ function generateStateTree() {
+ let userTree = stateDefinitions.generateTree({
parent: 'users',
modes: ['add', 'edit'],
list: 'UserList',
@@ -44,7 +48,28 @@ angular.module('Users', [])
ncyBreadcrumb: {
label: N_('USERS')
}
- })
+ });
+
+ return Promise.all([
+ userTree
+ ]).then((generated) => {
+ return {
+ states: _.reduce(generated, (result, definition) => {
+ return result.concat(definition.states);
+ }, [
+ stateExtender.buildDefinition(UserTokensListRoute),
+ stateExtender.buildDefinition(UserTokensAddRoute),
+ stateExtender.buildDefinition(UserTokensAddApplicationRoute)
+ ])
+ };
+ });
+ }
+
+ $stateProvider.state({
+ name: 'users.**',
+ url: '/users',
+ lazyLoad: () => generateStateTree()
});
+
}
]);
diff --git a/awx/ui/client/src/users/token-modal.block.less b/awx/ui/client/src/users/token-modal.block.less
new file mode 100644
index 0000000000..d913b3e3ef
--- /dev/null
+++ b/awx/ui/client/src/users/token-modal.block.less
@@ -0,0 +1,12 @@
+/** @define TokenModal */
+.TokenModal {
+ display: flex;
+}
+
+.TokenModal-label {
+ font-weight: bold;
+ width: 130px;
+}
+
+.TokenModal-value {
+}
diff --git a/awx/ui/client/src/users/tokens.strings.js b/awx/ui/client/src/users/tokens.strings.js
new file mode 100644
index 0000000000..2780661e79
--- /dev/null
+++ b/awx/ui/client/src/users/tokens.strings.js
@@ -0,0 +1,30 @@
+function TokensStrings (BaseString) {
+ BaseString.call(this, 'tokens');
+
+ const { t } = this;
+ const ns = this.tokens;
+
+ ns.state = {
+ LIST_BREADCRUMB_LABEL: t.s('TOKENS'),
+ ADD_BREADCRUMB_LABEL: t.s('CREATE TOKEN'),
+ USER_LIST_BREADCRUMB_LABEL: t.s('TOKENS')
+ };
+
+ ns.tab = {
+ DETAILS: t.s('Details')
+ };
+
+ ns.add = {
+ PANEL_TITLE: t.s('CREATE TOKEN')
+ };
+
+ ns.list = {
+ ROW_ITEM_LABEL_EXPIRED: t.s('DESCRIPTION'),
+ ROW_ITEM_LABEL_EXPIRED: t.s('EXPIRATION'),
+ ROW_ITEM_LABEL_USED: t.s('LAST USED')
+ };
+}
+
+TokensStrings.$inject = ['BaseStringService'];
+
+export default TokensStrings;
diff --git a/awx/ui/client/src/users/users-tokens-add-application.route.js b/awx/ui/client/src/users/users-tokens-add-application.route.js
new file mode 100644
index 0000000000..4ab5998d74
--- /dev/null
+++ b/awx/ui/client/src/users/users-tokens-add-application.route.js
@@ -0,0 +1,71 @@
+export default {
+ name: 'users.edit.tokens.add.application',
+ url: '/application?selected',
+ searchPrefix: 'application',
+ params: {
+ application_search: {
+ value: {
+ page_size: 5,
+ order_by: 'name'
+ },
+ dynamic: true,
+ squash: ''
+ }
+ },
+ data: {
+ basePath: 'applications',
+ formChildState: true
+ },
+ ncyBreadcrumb: {
+ skip: true
+ },
+ views: {
+ 'application@users.edit.tokens.add': {
+ templateProvider: (ListDefinition, generateList) => {
+ const html = generateList.build({
+ mode: 'lookup',
+ list: ListDefinition,
+ input_type: 'radio'
+ });
+
+ return `
${html}`;
+ }
+ }
+ },
+ resolve: {
+ ListDefinition: [() => {
+ return {
+ name: 'applications',
+ iterator: 'application',
+ hover: true,
+ index: false,
+ fields: {
+ name: {
+ key: true,
+ label: 'Name',
+ columnClass: 'col-lg-4 col-md-6 col-sm-8 col-xs-8',
+ awToolTip: '{{application.description | sanitize}}',
+ dataPlacement: 'top'
+ },
+ },
+ actions: {
+ },
+ fieldActions: {
+ }
+ }
+ }],
+ Dataset: ['QuerySet', 'GetBasePath', '$stateParams', 'ListDefinition',
+ (qs, GetBasePath, $stateParams, list) => qs.search(
+ GetBasePath('applications'),
+ $stateParams[`${list.iterator}_search`]
+ )
+ ]
+ },
+ onExit ($state) {
+ if ($state.transition) {
+ $('#form-modal').modal('hide');
+ $('.modal-backdrop').remove();
+ $('body').removeClass('modal-open');
+ }
+ }
+}
diff --git a/awx/ui/client/src/users/users-tokens-add.controller.js b/awx/ui/client/src/users/users-tokens-add.controller.js
new file mode 100644
index 0000000000..7b63a8ce61
--- /dev/null
+++ b/awx/ui/client/src/users/users-tokens-add.controller.js
@@ -0,0 +1,99 @@
+function AddTokensController (models, $state, strings, Rest, Alert, Wait, GetBasePath, $filter) {
+ const vm = this || {};
+ const { application } = models;
+
+ vm.mode = 'add';
+ vm.strings = strings;
+ vm.panelTitle = strings.get('add.PANEL_TITLE');
+
+ vm.form = {};
+
+ vm.form.application = {
+ type: 'field',
+ label: 'Application',
+ id: 'application'
+ };
+ vm.form.description = {
+ type: 'String',
+ label: 'Description',
+ id: 'description'
+ };
+
+ vm.form.application._resource = 'application';
+ vm.form.application._route = 'users.edit.tokens.add.application';
+ vm.form.application._model = application;
+ vm.form.application._placeholder = strings.get('SELECT AN APPLICATION');
+ vm.form.application.required = true;
+
+ vm.form.description.required = false;
+
+ vm.form.scope = {
+ choices: ['', 'read', 'write'],
+ help_text: 'Specify a scope for the token\'s access',
+ id: 'scope',
+ label: 'Scope',
+ required: true,
+ _component: 'at-input-select',
+ _data: ['', 'read', 'write'],
+ _exp: 'choice for (index, choice) in state._data',
+ _format: 'array'
+ }
+
+ vm.form.save = data => {
+ Rest.setUrl(GetBasePath('users') + $state.params.user_id + '/authorized_tokens');
+ return Rest.post(data)
+ .then(({data}) => {
+ Alert('TOKEN INFORMATION', `
+
+
+ TOKEN
+
+
+ ${data.token}
+
+
+
+
+ REFRESH TOKEN
+
+
+ ${data.refresh_token}
+
+
+
+
+ EXPIRES
+
+
+ ${$filter('longDate')(data.expires)}
+
+
+ `, null, null, null, null, null, true);
+ Wait('stop');
+ })
+ .catch(({data, status}) => {
+ ProcessErrors($scope, data, status, null, {
+ hdr: 'COULD NOT CREATE TOKEN',
+ msg: `Returned status: ${status}`
+ });
+ Wait('stop');
+ });
+ };
+
+ vm.form.onSaveSuccess = res => {
+ $state.go('^', { user_id: $state.params.user_id }, { reload: true });
+ };
+}
+
+AddTokensController.$inject = [
+ 'resolvedModels',
+ '$state',
+ 'ApplicationsStrings',
+ 'Rest',
+ 'Alert',
+ 'Wait',
+ 'GetBasePath',
+ '$filter'
+];
+
+export default AddTokensController;
diff --git a/awx/ui/client/src/users/users-tokens-add.partial.html b/awx/ui/client/src/users/users-tokens-add.partial.html
new file mode 100644
index 0000000000..21f5a8be04
--- /dev/null
+++ b/awx/ui/client/src/users/users-tokens-add.partial.html
@@ -0,0 +1,18 @@
+
+
+ {{ vm.panelTitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/src/users/users-tokens-add.route.js b/awx/ui/client/src/users/users-tokens-add.route.js
new file mode 100644
index 0000000000..e3832133a9
--- /dev/null
+++ b/awx/ui/client/src/users/users-tokens-add.route.js
@@ -0,0 +1,38 @@
+import { N_ } from '../i18n';
+import AddController from './users-tokens-add.controller';
+
+const addTemplate = require('~src/users/users-tokens-add.partial.html');
+
+function TokensDetailResolve ($q, Application) {
+
+ const promises = {};
+
+ promises.application = new Application('options');
+
+ return $q.all(promises);
+}
+
+TokensDetailResolve.$inject = [
+ '$q',
+ 'ApplicationModel'
+];
+
+export default {
+ url: "/add-token",
+ name: 'users.edit.tokens.add',
+ params: {
+ },
+ ncyBreadcrumb: {
+ label: N_("ADD TOKEN")
+ },
+ views: {
+ 'preFormView@users.edit': {
+ templateUrl: addTemplate,
+ controller: AddController,
+ controllerAs: 'vm'
+ }
+ },
+ resolve: {
+ resolvedModels: TokensDetailResolve
+ }
+};
diff --git a/awx/ui/client/src/users/users-tokens-list.controller.js b/awx/ui/client/src/users/users-tokens-list.controller.js
new file mode 100644
index 0000000000..bee39f45ae
--- /dev/null
+++ b/awx/ui/client/src/users/users-tokens-list.controller.js
@@ -0,0 +1,119 @@
+/** ***********************************************
+ * Copyright (c) 2018 Ansible, Inc.
+ *
+ * All Rights Reserved
+ ************************************************ */
+function ListTokensController (
+ $filter,
+ $scope,
+ $state,
+ Dataset,
+ strings,
+ ProcessErrors,
+ Rest,
+ GetBasePath,
+ Prompt,
+ Wait
+) {
+ const vm = this || {};
+
+ vm.strings = strings;
+ vm.activeId = $state.params.token_id;
+
+ $scope.canAdd = true;
+
+ // smart-search
+ const name = 'tokens';
+ const iterator = 'token';
+ const key = 'token_dataset';
+
+ $scope.list = { iterator, name, basePath: 'tokens' };
+ $scope.collection = { iterator };
+ $scope[key] = Dataset.data;
+ vm.tokensCount = Dataset.data.count;
+ $scope[name] = Dataset.data.results;
+ $scope.$on('updateDataset', (e, dataset) => {
+ $scope[key] = dataset;
+ $scope[name] = dataset.results;
+ vm.tokensCount = dataset.count;
+ });
+
+ vm.getLastUsed = token => {
+ const lastUsed = _.get(token, 'last_used');
+
+ if (!lastUsed) {
+ return undefined;
+ }
+
+ let html = $filter('longDate')(lastUsed);
+
+ const { username, id } = _.get(user, 'summary_fields.last_used', {});
+
+ if (username && id) {
+ html += ` by
${$filter('sanitize')(username)}`;
+ }
+
+ return html;
+ };
+
+ vm.deleteToken = (tok) => {
+ const action = () => {
+ $('#prompt-modal').modal('hide');
+ Wait('start');
+ Rest.setUrl(`${GetBasePath('tokens')}${tok.id}`);
+ Rest.destroy()
+ .then(() => {
+ let reloadListStateParams = null;
+
+ if ($scope.tokens.length === 1 && $state.params.token_search &&
+ !_.isEmpty($state.params.token_search.page) &&
+ $state.params.token_search.page !== '1') {
+ const page = `${(parseInt(reloadListStateParams
+ .token_search.page, 10) - 1)}`;
+ reloadListStateParams = _.cloneDeep($state.params);
+ reloadListStateParams.token_search.page = page;
+ }
+
+ if (parseInt($state.params.token_id, 10) === tok.id) {
+ $state.go('^', reloadListStateParams, { reload: true });
+ } else {
+ $state.go('.', reloadListStateParams, { reload: true });
+ }
+ })
+ .catch(({ data, status }) => {
+ ProcessErrors($scope, data, status, null, {
+ hdr: strings.get('error.HEADER'),
+ msg: strings.get('error.CALL', { path: `${GetBasePath('tokens')}${tok.id}`, status })
+ });
+ })
+ .finally(() => {
+ Wait('stop');
+ });
+ };
+
+ const deleteModalBody = `
${strings.get('deleteResource.CONFIRM', 'token')}
`;
+
+ Prompt({
+ hdr: strings.get('deleteResource.HEADER'),
+ resourceName: 'token',
+ body: deleteModalBody,
+ action,
+ actionText: 'DELETE'
+ });
+ };
+}
+
+ListTokensController.$inject = [
+ '$filter',
+ '$scope',
+ '$state',
+ 'Dataset',
+ 'TokensStrings',
+ 'ProcessErrors',
+ 'Rest',
+ 'GetBasePath',
+ 'Prompt',
+ 'Wait'
+];
+
+export default ListTokensController;
diff --git a/awx/ui/client/src/users/users-tokens-list.partial.html b/awx/ui/client/src/users/users-tokens-list.partial.html
new file mode 100644
index 0000000000..390361faba
--- /dev/null
+++ b/awx/ui/client/src/users/users-tokens-list.partial.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/src/users/users-tokens-list.route.js b/awx/ui/client/src/users/users-tokens-list.route.js
new file mode 100644
index 0000000000..81ec099adc
--- /dev/null
+++ b/awx/ui/client/src/users/users-tokens-list.route.js
@@ -0,0 +1,48 @@
+import { N_ } from '../i18n';
+
+import ListController from './users-tokens-list.controller';
+
+const listTemplate = require('~src/users/users-tokens-list.partial.html');
+
+export default {
+ url: "/tokens",
+ name: 'users.edit.tokens',
+ params: {
+ },
+ ncyBreadcrumb: {
+ label: N_("TOKENS")
+ },
+ views: {
+ 'related': {
+ templateUrl: listTemplate,
+ controller: ListController,
+ controllerAs: 'vm'
+ }
+ },
+ searchPrefix: 'token',
+ params: {
+ token_search: {
+ value: {
+ page_size: 5,
+ order_by: 'application'
+ }
+ }
+ },
+ resolve: {
+ Dataset: [
+ '$stateParams',
+ 'Wait',
+ 'GetBasePath',
+ 'QuerySet',
+ ($stateParams, Wait, GetBasePath, qs) => {
+ const searchParam = $stateParams.token_search;
+ const searchPath = GetBasePath('users') + $stateParams.user_id + '/tokens';
+ Wait('start');
+ return qs.search(searchPath, searchParam)
+ .finally(() => {
+ Wait('stop');
+ });
+ }
+ ],
+ }
+};
diff --git a/awx/ui/client/src/users/users.form.js b/awx/ui/client/src/users/users.form.js
index 521a2cb75e..78ea4f33e4 100644
--- a/awx/ui/client/src/users/users.form.js
+++ b/awx/ui/client/src/users/users.form.js
@@ -227,6 +227,11 @@ export default ['i18n', function(i18n) {
}
},
//hideOnSuperuser: true // RBAC defunct
+ },
+ tokens: {
+ ngIf: 'isCurrentlyLoggedInUser',
+ title: i18n._('Tokens'),
+ skipGenerator: true,
}
}