diff --git a/awx/ui/client/features/_index.less b/awx/ui/client/features/_index.less index 5046881522..e2339dc9e4 100644 --- a/awx/ui/client/features/_index.less +++ b/awx/ui/client/features/_index.less @@ -1 +1,2 @@ @import 'credentials/_index'; +@import 'users/tokens/_index'; diff --git a/awx/ui/client/features/index.js b/awx/ui/client/features/index.js index 75093bf39c..01216e575f 100644 --- a/awx/ui/client/features/index.js +++ b/awx/ui/client/features/index.js @@ -5,6 +5,7 @@ import atLibModels from '~models'; import atFeaturesApplications from '~features/applications'; import atFeaturesCredentials from '~features/credentials'; import atFeaturesTemplates from '~features/templates'; +import atFeaturesUsers from '~features/users'; const MODULE_NAME = 'at.features'; @@ -14,7 +15,8 @@ angular.module(MODULE_NAME, [ atLibModels, atFeaturesApplications, atFeaturesCredentials, - atFeaturesTemplates + atFeaturesTemplates, + atFeaturesUsers ]); export default MODULE_NAME; diff --git a/awx/ui/client/features/users/index.js b/awx/ui/client/features/users/index.js new file mode 100644 index 0000000000..b8f6a8052f --- /dev/null +++ b/awx/ui/client/features/users/index.js @@ -0,0 +1,8 @@ +import atFeaturesUsersTokens from '~features/users/tokens'; + +const MODULE_NAME = 'at.features.users'; + +angular + .module(MODULE_NAME, [atFeaturesUsersTokens]); + +export default MODULE_NAME; diff --git a/awx/ui/client/features/users/tokens/_index.less b/awx/ui/client/features/users/tokens/_index.less new file mode 100644 index 0000000000..f1052b5fa6 --- /dev/null +++ b/awx/ui/client/features/users/tokens/_index.less @@ -0,0 +1,9 @@ +/** @define TokenModal */ +.TokenModal { + display: flex; +} + +.TokenModal-label { + font-weight: bold; + width: 130px; +} diff --git a/awx/ui/client/features/users/tokens/index.js b/awx/ui/client/features/users/tokens/index.js new file mode 100644 index 0000000000..6d635b572d --- /dev/null +++ b/awx/ui/client/features/users/tokens/index.js @@ -0,0 +1,9 @@ +import TokensStrings from './tokens.strings'; + +const MODULE_NAME = 'at.features.users.tokens'; + +angular + .module(MODULE_NAME, []) + .service('TokensStrings', TokensStrings); + +export default MODULE_NAME; diff --git a/awx/ui/client/features/users/tokens/tokens.strings.js b/awx/ui/client/features/users/tokens/tokens.strings.js new file mode 100644 index 0000000000..ac0af8e15f --- /dev/null +++ b/awx/ui/client/features/users/tokens/tokens.strings.js @@ -0,0 +1,43 @@ +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'), + APP_PLACEHOLDER: t.s('SELECT AN APPLICATION'), + SCOPE_HELP_TEXT: t.s('Specify a scope for the token\'s access'), + TOKEN_MODAL_HEADER: t.s('TOKEN INFORMATION'), + TOKEN_LABEL: t.s('TOKEN'), + REFRESH_TOKEN_LABEL: t.s('REFRESH TOKEN'), + TOKEN_EXPIRES_LABEL: t.s('EXPIRES'), + ERROR_HEADER: t.s('COULD NOT CREATE TOKEN'), + ERROR_BODY_LABEL: t.s('Returned status:'), + LAST_USED_LABEL: t.s('by'), + DELETE_ACTION_LABEL: t.s('DELETE'), + SCOPE_PLACEHOLDER: t.s('Select a scope'), + SCOPE_READ_LABEL: t.s('Read'), + SCOPE_WRITE_LABEL: t.s('Write') + }; + + ns.list = { + ROW_ITEM_LABEL_DESCRIPTION: 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/features/users/tokens/users-tokens-add-application.route.js b/awx/ui/client/features/users/tokens/users-tokens-add-application.route.js new file mode 100644 index 0000000000..13197ca11d --- /dev/null +++ b/awx/ui/client/features/users/tokens/users-tokens-add-application.route.js @@ -0,0 +1,69 @@ +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: [() => ({ + 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/features/users/tokens/users-tokens-add.controller.js b/awx/ui/client/features/users/tokens/users-tokens-add.controller.js new file mode 100644 index 0000000000..1421077b1c --- /dev/null +++ b/awx/ui/client/features/users/tokens/users-tokens-add.controller.js @@ -0,0 +1,111 @@ +function AddTokensController ( + models, $state, strings, Rest, Alert, Wait, GetBasePath, + $filter, ProcessErrors +) { + 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('add.APP_PLACEHOLDER'); + vm.form.application.required = true; + + vm.form.description.required = false; + + vm.form.scope = { + choices: [ + '', + 'read', + 'write' + ], + help_text: strings.get('add.SCOPE_HELP_TEXT'), + id: 'scope', + label: 'Scope', + required: true, + _component: 'at-input-select', + _data: [ + strings.get('add.SCOPE_PLACEHOLDER'), + strings.get('add.SCOPE_READ_LABEL'), + strings.get('add.SCOPE_WRITE_LABEL') + ], + _exp: 'choice for (index, choice) in state._data', + _format: 'array' + }; + + vm.form.save = payload => { + Rest.setUrl(`${GetBasePath('users')}${$state.params.user_id}/authorized_tokens`); + return Rest.post(payload) + .then(({ data }) => { + Alert(strings.get('add.TOKEN_MODAL_HEADER'), ` +
+
+ ${strings.get('add.TOKEN_LABEL')} +
+
+ ${data.token} +
+
+
+
+ ${strings.get('add.REFRESH_TOKEN_LABEL')} +
+
+ ${data.refresh_token} +
+
+
+
+ ${strings.get('add.TOKEN_EXPIRES_LABEL')} +
+
+ ${$filter('longDate')(data.expires)} +
+
+ `, null, null, null, null, null, true); + Wait('stop'); + }) + .catch(({ data, status }) => { + ProcessErrors(null, data, status, null, { + hdr: strings.get('add.ERROR_HEADER'), + msg: `${strings.get('add.ERROR_BODY_LABEL')} ${status}` + }); + Wait('stop'); + }); + }; + + vm.form.onSaveSuccess = () => { + $state.go('^', { user_id: $state.params.user_id }, { reload: true }); + }; +} + +AddTokensController.$inject = [ + 'resolvedModels', + '$state', + 'TokensStrings', + 'Rest', + 'Alert', + 'Wait', + 'GetBasePath', + '$filter', + 'ProcessErrors' +]; + +export default AddTokensController; diff --git a/awx/ui/client/features/users/tokens/users-tokens-add.partial.html b/awx/ui/client/features/users/tokens/users-tokens-add.partial.html new file mode 100644 index 0000000000..21f5a8be04 --- /dev/null +++ b/awx/ui/client/features/users/tokens/users-tokens-add.partial.html @@ -0,0 +1,18 @@ + + + {{ vm.panelTitle }} + + + + + + + + + + + + + + + diff --git a/awx/ui/client/features/users/tokens/users-tokens-add.route.js b/awx/ui/client/features/users/tokens/users-tokens-add.route.js new file mode 100644 index 0000000000..350e9bd2dd --- /dev/null +++ b/awx/ui/client/features/users/tokens/users-tokens-add.route.js @@ -0,0 +1,37 @@ +import { N_ } from '../../../src/i18n'; +import AddController from './users-tokens-add.controller'; + +const addTemplate = require('~features/users/tokens/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_('CREATE TOKEN') + }, + views: { + 'preFormView@users.edit': { + templateUrl: addTemplate, + controller: AddController, + controllerAs: 'vm' + } + }, + resolve: { + resolvedModels: TokensDetailResolve + } +}; diff --git a/awx/ui/client/features/users/tokens/users-tokens-list.controller.js b/awx/ui/client/features/users/tokens/users-tokens-list.controller.js new file mode 100644 index 0000000000..c2fe647928 --- /dev/null +++ b/awx/ui/client/features/users/tokens/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(token, 'summary_fields.last_used', {}); + + if (username && id) { + html += ` ${strings.get('add.LAST_USED_LABEL')} ${$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: strings.get('add.DELETE_ACTION_LABEL') + }); + }; +} + +ListTokensController.$inject = [ + '$filter', + '$scope', + '$state', + 'Dataset', + 'TokensStrings', + 'ProcessErrors', + 'Rest', + 'GetBasePath', + 'Prompt', + 'Wait' +]; + +export default ListTokensController; diff --git a/awx/ui/client/features/users/tokens/users-tokens-list.partial.html b/awx/ui/client/features/users/tokens/users-tokens-list.partial.html new file mode 100644 index 0000000000..390361faba --- /dev/null +++ b/awx/ui/client/features/users/tokens/users-tokens-list.partial.html @@ -0,0 +1,47 @@ +
+ + +
+ +
+
+ + + +
+ + + + + + + + +
+
+ + +
+
+
diff --git a/awx/ui/client/features/users/tokens/users-tokens-list.route.js b/awx/ui/client/features/users/tokens/users-tokens-list.route.js new file mode 100644 index 0000000000..c8a780e473 --- /dev/null +++ b/awx/ui/client/features/users/tokens/users-tokens-list.route.js @@ -0,0 +1,46 @@ +import { N_ } from '../../../src/i18n'; + +import ListController from './users-tokens-list.controller'; + +const listTemplate = require('~features/users/tokens/users-tokens-list.partial.html'); + +export default { + url: '/tokens', + name: 'users.edit.tokens', + 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/lib/components/input/select.partial.html b/awx/ui/client/lib/components/input/select.partial.html index aaa31bebee..f3ba5e845a 100644 --- a/awx/ui/client/lib/components/input/select.partial.html +++ b/awx/ui/client/lib/components/input/select.partial.html @@ -12,7 +12,7 @@