1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 16:51:11 +03:00

Merge pull request #1580 from jlmitch5/usersAppCrudUi

implement users tokens sub list
This commit is contained in:
John Mitchell 2018-03-19 13:23:29 -04:00 committed by GitHub
commit a9e13cc5f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 567 additions and 14 deletions

View File

@ -1 +1,2 @@
@import 'credentials/_index';
@import 'users/tokens/_index';

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,9 @@
/** @define TokenModal */
.TokenModal {
display: flex;
}
.TokenModal-label {
font-weight: bold;
width: 130px;
}

View File

@ -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;

View File

@ -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;

View File

@ -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 `<lookup-modal>${html}</lookup-modal>`;
}
}
},
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');
}
}
};

View File

@ -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'), `
<div class="TokenModal">
<div class="TokenModal-label">
${strings.get('add.TOKEN_LABEL')}
</div>
<div class="TokenModal-value">
${data.token}
</div>
</div>
<div class="TokenModal">
<div class="TokenModal-label">
${strings.get('add.REFRESH_TOKEN_LABEL')}
</div>
<div class="TokenModal-value">
${data.refresh_token}
</div>
</div>
<div class="TokenModal">
<div class="TokenModal-label">
${strings.get('add.TOKEN_EXPIRES_LABEL')}
</div>
<div class="TokenModal-value">
${$filter('longDate')(data.expires)}
</div>
</div>
`, 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;

View File

@ -0,0 +1,18 @@
<at-panel>
<at-panel-heading>
{{ vm.panelTitle }}
</at-panel-heading>
<at-panel-body>
<at-form state="vm.form" autocomplete="off">
<at-input-lookup col="4" tab="1" state="vm.form.application"></at-input-lookup>
<at-input-text col="4" tab="2" state="vm.form.description"></at-input-text>
<at-input-select col="4" tab="3" state="vm.form.scope"></at-input-select>
<at-action-group col="12" pos="right">
<at-form-action type="cancel" to="tokens"></at-form-action>
<at-form-action type="save"></at-form-action>
</at-action-group>
</at-form>
</at-panel-body>
</at-panel>

View File

@ -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
}
};

View File

@ -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')} <a href="/#/users/${id}">${$filter('sanitize')(username)}</a>`;
}
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 = `<div class="Prompt-bodyQuery">${strings.get('deleteResource.CONFIRM', 'token')}</div>`;
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;

View File

@ -0,0 +1,47 @@
<div class="at-List-toolbar">
<smart-search
class="at-List-search"
django-model="tokens"
base-path="tokens"
iterator="token"
list="list"
dataset="token_dataset"
collection="collection"
search-tags="searchTags">
</smart-search>
<div class="at-List-toolbarAction">
<button
type="button"
ui-sref=".add"
class="at-Button--add"
aria-haspopup="true"
aria-expanded="false">
</button>
</div>
</div>
<at-list results="tokens">
<at-row ng-repeat="token in tokens">
<div class="at-Row-items">
<at-row-item
header-value="{{ token.summary_fields.application.name }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_DESCRIPTION') }}"
value="{{ token.description }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_EXPIRED') }}"
value="{{ token.expriation | longDate }}">
</at-row-item>
<at-row-item
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_USED') }}"
value="{{ vm.getLastUsed(token) }}">
</at-row-item>
</div>
<div class="at-Row-actions">
<at-row-action icon="fa-trash" ng-click="vm.deleteToken(token)">
</at-row-action>
</div>
</at-row>
</at-list>

View File

@ -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');
});
}
],
}
};

View File

@ -12,7 +12,7 @@
<select class="form-control at-InputSelect-select"
ng-model="state._value"
ng-attr-tabindex="{{ tab || undefined }}"
ng-attr-tabindex="{{ tab || undefined }}"
ng-disabled="state._disabled || form.disabled"
ng-options="{{ state._exp }}">
<option style="display:none"></option>

View File

@ -166,14 +166,18 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
// Also wraps mess of generated HTML in a .Panel
wrapPanel(html, ignorePanel){
if(ignorePanel) {
return `<div>
return `
<div ui-view="preFormView"></div>
<div>
${html}
<div ui-view="related"></div>
<div ui-view="modal"></div>
</div>`;
}
else {
return `<div class="Panel">
return `
<div ui-view="preFormView"></div>
<div class="Panel">
${html}
<div ui-view="related"></div>
<div ui-view="modal"></div>

View File

@ -583,7 +583,8 @@ export default ['$compile', 'Attr', 'Icon',
},
wrapPanel: function(html){
return `<div class="Panel">${html}</div>`;
return `
<div class="Panel">${html}</div>`;
},
insertFormView: function(){

View File

@ -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;

View File

@ -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 '../../features/users/tokens/users-tokens-list.route';
import UserTokensAddRoute from '../../features/users/tokens/users-tokens-add.route';
import UserTokensAddApplicationRoute from '../../features/users/tokens/users-tokens-add-application.route';
import { N_ } from '../i18n';
export default
@ -18,16 +23,13 @@ angular.module('Users', [])
.controller('UsersEdit', UsersEdit)
.factory('UserForm', UserForm)
.factory('UserList', UserList)
.config(['$stateProvider', 'stateDefinitionsProvider',
function($stateProvider, stateDefinitionsProvider) {
.config(['$stateProvider', 'stateDefinitionsProvider', '$stateExtenderProvider',
function($stateProvider, stateDefinitionsProvider, $stateExtenderProvider) {
let stateDefinitions = stateDefinitionsProvider.$get();
let stateExtender = $stateExtenderProvider.$get();
// 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({
function generateStateTree() {
let userTree = stateDefinitions.generateTree({
parent: 'users',
modes: ['add', 'edit'],
list: 'UserList',
@ -44,7 +46,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()
});
}
]);

View File

@ -227,6 +227,11 @@ export default ['i18n', function(i18n) {
}
},
//hideOnSuperuser: true // RBAC defunct
},
tokens: {
ngIf: 'isCurrentlyLoggedInUser',
title: i18n._('Tokens'),
skipGenerator: true,
}
}