mirror of
https://github.com/ansible/awx.git
synced 2026-02-27 15:58:45 -03:30
Merge pull request #1437 from jlmitch5/appCrudUi
implements application crud ui
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
function AddApplicationsController (models, $state, strings) {
|
||||||
|
const vm = this || {};
|
||||||
|
|
||||||
|
const { application, me, organization } = models;
|
||||||
|
const omit = [
|
||||||
|
'authorization_grant_type',
|
||||||
|
'client_id',
|
||||||
|
'client_secret',
|
||||||
|
'client_type',
|
||||||
|
'created',
|
||||||
|
'modified',
|
||||||
|
'related',
|
||||||
|
'skip_authorization',
|
||||||
|
'summary_fields',
|
||||||
|
'type',
|
||||||
|
'url',
|
||||||
|
'user'
|
||||||
|
];
|
||||||
|
|
||||||
|
vm.mode = 'add';
|
||||||
|
vm.strings = strings;
|
||||||
|
vm.panelTitle = strings.get('add.PANEL_TITLE');
|
||||||
|
|
||||||
|
vm.tab = {
|
||||||
|
details: { _active: true },
|
||||||
|
users: { _disabled: true }
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.form = application.createFormSchema('post', { omit });
|
||||||
|
|
||||||
|
vm.form.organization = {
|
||||||
|
type: 'field',
|
||||||
|
label: 'Organization',
|
||||||
|
id: 'organization'
|
||||||
|
};
|
||||||
|
vm.form.description = {
|
||||||
|
type: 'String',
|
||||||
|
label: 'Description',
|
||||||
|
id: 'description'
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.form.disabled = !application.isCreatable();
|
||||||
|
|
||||||
|
vm.form.organization._resource = 'organization';
|
||||||
|
vm.form.organization._route = 'applications.add.organization';
|
||||||
|
vm.form.organization._model = organization;
|
||||||
|
vm.form.organization._placeholder = strings.get('SELECT AN ORGANIZATION');
|
||||||
|
|
||||||
|
vm.form.name.required = true;
|
||||||
|
vm.form.organization.required = true;
|
||||||
|
vm.form.redirect_uris.required = true;
|
||||||
|
|
||||||
|
delete vm.form.name.help_text;
|
||||||
|
|
||||||
|
vm.form.save = data => {
|
||||||
|
const hiddenData = {
|
||||||
|
authorization_grant_type: 'implicit',
|
||||||
|
user: me.get('id'),
|
||||||
|
client_type: 'public'
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = _.merge(data, hiddenData);
|
||||||
|
|
||||||
|
return application.request('post', { data: payload });
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.form.onSaveSuccess = res => {
|
||||||
|
$state.go('applications.edit', { application_id: res.data.id }, { reload: true });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
AddApplicationsController.$inject = [
|
||||||
|
'resolvedModels',
|
||||||
|
'$state',
|
||||||
|
'ApplicationsStrings'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default AddApplicationsController;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<at-panel>
|
||||||
|
<at-panel-heading>
|
||||||
|
{{ vm.panelTitle }}
|
||||||
|
</at-panel-heading>
|
||||||
|
|
||||||
|
<at-tab-group>
|
||||||
|
<at-tab state="vm.tab.details">{{:: vm.strings.get('tab.DETAILS') }}</at-tab>
|
||||||
|
<at-tab state="vm.tab.users">{{:: vm.strings.get('tab.USERS') }}</at-tab>
|
||||||
|
</at-tab-group>
|
||||||
|
|
||||||
|
<at-panel-body ng-if="!$state.current.name.includes('users')">
|
||||||
|
<at-form state="vm.form" autocomplete="off">
|
||||||
|
<at-input-text col="4" tab="1" state="vm.form.name"></at-input-text>
|
||||||
|
<at-input-text col="4" tab="2" state="vm.form.description"></at-input-text>
|
||||||
|
<at-input-lookup col="4" tab="3" state="vm.form.organization"></at-input-lookup>
|
||||||
|
<at-divider></at-divider>
|
||||||
|
<at-input-text col="4" tab="4" state="vm.form.redirect_uris"></at-input-text>
|
||||||
|
|
||||||
|
<at-action-group col="12" pos="right">
|
||||||
|
<at-form-action type="cancel" to="applications"></at-form-action>
|
||||||
|
<at-form-action type="save"></at-form-action>
|
||||||
|
</at-action-group>
|
||||||
|
</at-form>
|
||||||
|
</at-panel-body>
|
||||||
|
|
||||||
|
<at-panel-body ng-if="$state.current.name.includes('users')">
|
||||||
|
<div ui-view="userList"></div>
|
||||||
|
</at-panel-body>
|
||||||
|
</at-panel>
|
||||||
32
awx/ui/client/features/applications/applications.strings.js
Normal file
32
awx/ui/client/features/applications/applications.strings.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
function ApplicationsStrings (BaseString) {
|
||||||
|
BaseString.call(this, 'applications');
|
||||||
|
|
||||||
|
const { t } = this;
|
||||||
|
const ns = this.applications;
|
||||||
|
|
||||||
|
ns.state = {
|
||||||
|
LIST_BREADCRUMB_LABEL: t.s('APPLICATIONS'),
|
||||||
|
ADD_BREADCRUMB_LABEL: t.s('CREATE APPLICATION'),
|
||||||
|
EDIT_BREADCRUMB_LABEL: t.s('EDIT APPLICATION'),
|
||||||
|
USER_LIST_BREADCRUMB_LABEL: t.s('TOKENS')
|
||||||
|
};
|
||||||
|
|
||||||
|
ns.tab = {
|
||||||
|
DETAILS: t.s('Details'),
|
||||||
|
USERS: t.s('Tokens')
|
||||||
|
};
|
||||||
|
|
||||||
|
ns.add = {
|
||||||
|
PANEL_TITLE: t.s('NEW APPLICATION')
|
||||||
|
};
|
||||||
|
|
||||||
|
ns.list = {
|
||||||
|
ROW_ITEM_LABEL_EXPIRED: t.s('EXPIRATION'),
|
||||||
|
ROW_ITEM_LABEL_ORGANIZATION: t.s('ORG'),
|
||||||
|
ROW_ITEM_LABEL_MODIFIED: t.s('LAST MODIFIED')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationsStrings.$inject = ['BaseStringService'];
|
||||||
|
|
||||||
|
export default ApplicationsStrings;
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
function EditApplicationsController (models, $state, strings, $scope) {
|
||||||
|
const vm = this || {};
|
||||||
|
|
||||||
|
const { me, application, organization } = models;
|
||||||
|
|
||||||
|
const omit = [
|
||||||
|
'authorization_grant_type',
|
||||||
|
'client_id',
|
||||||
|
'client_secret',
|
||||||
|
'client_type',
|
||||||
|
'created',
|
||||||
|
'modified',
|
||||||
|
'related',
|
||||||
|
'skip_authorization',
|
||||||
|
'summary_fields',
|
||||||
|
'type',
|
||||||
|
'url',
|
||||||
|
'user'
|
||||||
|
];
|
||||||
|
const isEditable = application.isEditable();
|
||||||
|
|
||||||
|
vm.mode = 'edit';
|
||||||
|
vm.strings = strings;
|
||||||
|
vm.panelTitle = application.get('name');
|
||||||
|
|
||||||
|
vm.tab = {
|
||||||
|
details: {
|
||||||
|
_active: true,
|
||||||
|
_go: 'applications.edit',
|
||||||
|
_params: { application_id: application.get('id') }
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
_go: 'applications.edit.users',
|
||||||
|
_params: { application_id: application.get('id') }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('$state.current.name', (value) => {
|
||||||
|
if (/applications.edit.users/.test(value)) {
|
||||||
|
vm.tab.details._active = false;
|
||||||
|
vm.tab.users._active = true;
|
||||||
|
} else {
|
||||||
|
vm.tab.details._active = true;
|
||||||
|
vm.tab.users._active = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isEditable) {
|
||||||
|
vm.form = application.createFormSchema('put', { omit });
|
||||||
|
} else {
|
||||||
|
vm.form = application.createFormSchema({ omit });
|
||||||
|
vm.form.disabled = !isEditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.form.disabled = !isEditable;
|
||||||
|
|
||||||
|
const isOrgAdmin = _.some(me.get('related.admin_of_organizations.results'), (org) => org.id === organization.get('id'));
|
||||||
|
const isSuperuser = me.get('is_superuser');
|
||||||
|
const isCurrentAuthor = Boolean(application.get('summary_fields.created_by.id') === me.get('id'));
|
||||||
|
|
||||||
|
vm.form.organization = {
|
||||||
|
type: 'field',
|
||||||
|
label: 'Organization',
|
||||||
|
id: 'organization'
|
||||||
|
};
|
||||||
|
vm.form.description = {
|
||||||
|
type: 'String',
|
||||||
|
label: 'Description',
|
||||||
|
id: 'description'
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.form.organization._resource = 'organization';
|
||||||
|
vm.form.organization._route = 'applications.edit.organization';
|
||||||
|
vm.form.organization._model = organization;
|
||||||
|
vm.form.organization._placeholder = strings.get('SELECT AN ORGANIZATION');
|
||||||
|
|
||||||
|
// TODO: org not returned via api endpoint, check on this
|
||||||
|
vm.form.organization._value = application.get('organization');
|
||||||
|
|
||||||
|
vm.form.organization._disabled = true;
|
||||||
|
if (isSuperuser || isOrgAdmin || (application.get('organization') === null && isCurrentAuthor)) {
|
||||||
|
vm.form.organization._disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.form.name.required = true;
|
||||||
|
vm.form.organization.required = true;
|
||||||
|
vm.form.redirect_uris.required = true;
|
||||||
|
|
||||||
|
delete vm.form.name.help_text;
|
||||||
|
|
||||||
|
vm.form.save = data => {
|
||||||
|
const hiddenData = {
|
||||||
|
authorization_grant_type: 'implicit',
|
||||||
|
user: me.get('id'),
|
||||||
|
client_type: 'public'
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = _.merge(data, hiddenData);
|
||||||
|
|
||||||
|
return application.request('put', { data: payload });
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.form.onSaveSuccess = () => {
|
||||||
|
$state.go('applications.edit', { application_id: application.get('id') }, { reload: true });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
EditApplicationsController.$inject = [
|
||||||
|
'resolvedModels',
|
||||||
|
'$state',
|
||||||
|
'ApplicationsStrings',
|
||||||
|
'$scope'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default EditApplicationsController;
|
||||||
325
awx/ui/client/features/applications/index.js
Normal file
325
awx/ui/client/features/applications/index.js
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
|
||||||
|
import AddController from './add-applications.controller';
|
||||||
|
import EditController from './edit-applications.controller';
|
||||||
|
import ListController from './list-applications.controller';
|
||||||
|
import UserListController from './list-applications-users.controller';
|
||||||
|
import ApplicationsStrings from './applications.strings';
|
||||||
|
|
||||||
|
const MODULE_NAME = 'at.features.applications';
|
||||||
|
|
||||||
|
const addEditTemplate = require('~features/applications/add-edit-applications.view.html');
|
||||||
|
const listTemplate = require('~features/applications/list-applications.view.html');
|
||||||
|
const indexTemplate = require('~features/applications/index.view.html');
|
||||||
|
const userListTemplate = require('~features/applications/list-applications-users.view.html');
|
||||||
|
|
||||||
|
function ApplicationsDetailResolve ($q, $stateParams, Me, Application, Organization) {
|
||||||
|
const id = $stateParams.application_id;
|
||||||
|
|
||||||
|
const promises = {
|
||||||
|
me: new Me('get').then((me) => me.extend('get', 'admin_of_organizations'))
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
promises.application = new Application('options');
|
||||||
|
promises.organization = new Organization();
|
||||||
|
|
||||||
|
return $q.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
promises.application = new Application(['get', 'options'], [id, id]);
|
||||||
|
|
||||||
|
return $q.all(promises)
|
||||||
|
.then(models => {
|
||||||
|
const orgId = models.application.get('organization');
|
||||||
|
|
||||||
|
const dependents = {
|
||||||
|
organization: new Organization('get', orgId)
|
||||||
|
};
|
||||||
|
|
||||||
|
return $q.all(dependents)
|
||||||
|
.then(related => {
|
||||||
|
models.organization = related.organization;
|
||||||
|
|
||||||
|
return models;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationsDetailResolve.$inject = [
|
||||||
|
'$q',
|
||||||
|
'$stateParams',
|
||||||
|
'MeModel',
|
||||||
|
'ApplicationModel',
|
||||||
|
'OrganizationModel'
|
||||||
|
];
|
||||||
|
|
||||||
|
function ApplicationsRun ($stateExtender, strings) {
|
||||||
|
$stateExtender.addState({
|
||||||
|
name: 'applications',
|
||||||
|
route: '/applications',
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
label: strings.get('state.LIST_BREADCRUMB_LABEL')
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
activityStream: true,
|
||||||
|
// TODO: double-check activity stream works
|
||||||
|
activityStreamTarget: 'application'
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
'@': {
|
||||||
|
templateUrl: indexTemplate,
|
||||||
|
},
|
||||||
|
'list@applications': {
|
||||||
|
templateUrl: listTemplate,
|
||||||
|
controller: ListController,
|
||||||
|
controllerAs: 'vm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
searchPrefix: 'application',
|
||||||
|
resolve: {
|
||||||
|
resolvedModels: [
|
||||||
|
'ApplicationModel',
|
||||||
|
(Application) => {
|
||||||
|
const app = new Application(['options']);
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Dataset: [
|
||||||
|
'$stateParams',
|
||||||
|
'Wait',
|
||||||
|
'GetBasePath',
|
||||||
|
'QuerySet',
|
||||||
|
($stateParams, Wait, GetBasePath, qs) => {
|
||||||
|
const searchParam = $stateParams.application_search;
|
||||||
|
const searchPath = GetBasePath('applications');
|
||||||
|
|
||||||
|
Wait('start');
|
||||||
|
return qs.search(searchPath, searchParam)
|
||||||
|
.finally(() => {
|
||||||
|
Wait('stop');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$stateExtender.addState({
|
||||||
|
name: 'applications.add',
|
||||||
|
route: '/add',
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
label: strings.get('state.ADD_BREADCRUMB_LABEL')
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
activityStream: true,
|
||||||
|
// TODO: double-check activity stream works
|
||||||
|
activityStreamTarget: 'application'
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
'add@applications': {
|
||||||
|
templateUrl: addEditTemplate,
|
||||||
|
controller: AddController,
|
||||||
|
controllerAs: 'vm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
resolvedModels: ApplicationsDetailResolve
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$stateExtender.addState({
|
||||||
|
name: 'applications.edit',
|
||||||
|
route: '/:application_id',
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
label: strings.get('state.EDIT_BREADCRUMB_LABEL')
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
activityStream: true,
|
||||||
|
activityStreamTarget: 'application',
|
||||||
|
activityStreamId: 'application_id'
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
'edit@applications': {
|
||||||
|
templateUrl: addEditTemplate,
|
||||||
|
controller: EditController,
|
||||||
|
controllerAs: 'vm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
resolvedModels: ApplicationsDetailResolve
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$stateExtender.addState({
|
||||||
|
name: 'applications.add.organization',
|
||||||
|
url: '/organization?selected',
|
||||||
|
searchPrefix: 'organization',
|
||||||
|
params: {
|
||||||
|
organization_search: {
|
||||||
|
value: {
|
||||||
|
page_size: 5,
|
||||||
|
order_by: 'name',
|
||||||
|
role_level: 'admin_role'
|
||||||
|
},
|
||||||
|
dynamic: true,
|
||||||
|
squash: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
basePath: 'organizations',
|
||||||
|
formChildState: true
|
||||||
|
},
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
skip: true
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
'organization@applications.add': {
|
||||||
|
templateProvider: (ListDefinition, generateList) => {
|
||||||
|
const html = generateList.build({
|
||||||
|
mode: 'lookup',
|
||||||
|
list: ListDefinition,
|
||||||
|
input_type: 'radio'
|
||||||
|
});
|
||||||
|
|
||||||
|
return `<lookup-modal>${html}</lookup-modal>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
ListDefinition: ['OrganizationList', list => list],
|
||||||
|
Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath',
|
||||||
|
(list, qs, $stateParams, GetBasePath) => qs.search(
|
||||||
|
GetBasePath('organizations'),
|
||||||
|
$stateParams[`${list.iterator}_search`]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
onExit ($state) {
|
||||||
|
if ($state.transition) {
|
||||||
|
$('#form-modal').modal('hide');
|
||||||
|
$('.modal-backdrop').remove();
|
||||||
|
$('body').removeClass('modal-open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$stateExtender.addState({
|
||||||
|
name: 'applications.edit.organization',
|
||||||
|
url: '/organization?selected',
|
||||||
|
searchPrefix: 'organization',
|
||||||
|
params: {
|
||||||
|
organization_search: {
|
||||||
|
value: {
|
||||||
|
page_size: 5,
|
||||||
|
order_by: 'name',
|
||||||
|
role_level: 'admin_role'
|
||||||
|
},
|
||||||
|
dynamic: true,
|
||||||
|
squash: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
basePath: 'organizations',
|
||||||
|
formChildState: true
|
||||||
|
},
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
skip: true
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
'organization@applications.edit': {
|
||||||
|
templateProvider: (ListDefinition, generateList) => {
|
||||||
|
const html = generateList.build({
|
||||||
|
mode: 'lookup',
|
||||||
|
list: ListDefinition,
|
||||||
|
input_type: 'radio'
|
||||||
|
});
|
||||||
|
|
||||||
|
return `<lookup-modal>${html}</lookup-modal>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
ListDefinition: ['OrganizationList', list => list],
|
||||||
|
Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath',
|
||||||
|
(list, qs, $stateParams, GetBasePath) => qs.search(
|
||||||
|
GetBasePath('organizations'),
|
||||||
|
$stateParams[`${list.iterator}_search`]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
onExit ($state) {
|
||||||
|
if ($state.transition) {
|
||||||
|
$('#form-modal').modal('hide');
|
||||||
|
$('.modal-backdrop').remove();
|
||||||
|
$('body').removeClass('modal-open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$stateExtender.addState({
|
||||||
|
name: 'applications.edit.users',
|
||||||
|
route: '/users',
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
label: strings.get('state.USER_LIST_BREADCRUMB_LABEL'),
|
||||||
|
parent: 'applications.edit'
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
activityStream: true,
|
||||||
|
// TODO: double-check activity stream works
|
||||||
|
activityStreamTarget: 'application'
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
'userList@applications.edit': {
|
||||||
|
templateUrl: userListTemplate,
|
||||||
|
controller: UserListController,
|
||||||
|
controllerAs: 'vm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
user_search: {
|
||||||
|
value: {
|
||||||
|
order_by: 'user',
|
||||||
|
page_size: 20
|
||||||
|
},
|
||||||
|
dynamic: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
searchPrefix: 'user',
|
||||||
|
resolve: {
|
||||||
|
resolvedModels: [
|
||||||
|
'ApplicationModel',
|
||||||
|
(Application) => {
|
||||||
|
const app = new Application(['options']);
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Dataset: [
|
||||||
|
'$stateParams',
|
||||||
|
'Wait',
|
||||||
|
'GetBasePath',
|
||||||
|
'QuerySet',
|
||||||
|
($stateParams, Wait, GetBasePath, qs) => {
|
||||||
|
const searchParam = $stateParams.user_search;
|
||||||
|
const searchPath = `${GetBasePath('applications')}${$stateParams.application_id}/tokens`;
|
||||||
|
|
||||||
|
Wait('start');
|
||||||
|
return qs.search(searchPath, searchParam)
|
||||||
|
.finally(() => {
|
||||||
|
Wait('stop');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationsRun.$inject = [
|
||||||
|
'$stateExtender',
|
||||||
|
'ApplicationsStrings'
|
||||||
|
];
|
||||||
|
|
||||||
|
angular
|
||||||
|
.module(MODULE_NAME, [])
|
||||||
|
.service('ApplicationsStrings', ApplicationsStrings)
|
||||||
|
.run(ApplicationsRun);
|
||||||
|
|
||||||
|
export default MODULE_NAME;
|
||||||
3
awx/ui/client/features/applications/index.view.html
Normal file
3
awx/ui/client/features/applications/index.view.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<div ui-view="edit"></div>
|
||||||
|
<div ui-view="add"></div>
|
||||||
|
<div ui-view="list"></div>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/** ***********************************************
|
||||||
|
* Copyright (c) 2018 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
************************************************ */
|
||||||
|
function ListApplicationsUsersController (
|
||||||
|
$filter,
|
||||||
|
$scope,
|
||||||
|
Dataset,
|
||||||
|
resolvedModels,
|
||||||
|
strings
|
||||||
|
) {
|
||||||
|
const vm = this || {};
|
||||||
|
// const application = resolvedModels;
|
||||||
|
|
||||||
|
vm.strings = strings;
|
||||||
|
// smart-search
|
||||||
|
const name = 'users';
|
||||||
|
const iterator = 'user';
|
||||||
|
const key = 'user_dataset';
|
||||||
|
|
||||||
|
$scope.list = { iterator, name, basePath: 'applications' };
|
||||||
|
$scope.collection = { iterator };
|
||||||
|
$scope[key] = Dataset.data;
|
||||||
|
vm.usersCount = Dataset.data.count;
|
||||||
|
$scope[name] = Dataset.data.results;
|
||||||
|
|
||||||
|
vm.getLastUsed = user => {
|
||||||
|
const lastUsed = _.get(user, '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 <a href="/#/users/${id}">${$filter('sanitize')(username)}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ListApplicationsUsersController.$inject = [
|
||||||
|
'$filter',
|
||||||
|
'$scope',
|
||||||
|
'Dataset',
|
||||||
|
'resolvedModels',
|
||||||
|
'ApplicationsStrings'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default ListApplicationsUsersController;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<div class="at-List-toolbar">
|
||||||
|
<smart-search
|
||||||
|
class="at-List-search"
|
||||||
|
django-model="users"
|
||||||
|
base-path="users"
|
||||||
|
iterator="user"
|
||||||
|
list="list"
|
||||||
|
dataset="user_dataset"
|
||||||
|
collection="collection"
|
||||||
|
search-tags="searchTags">
|
||||||
|
</smart-search>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<at-list results="users">
|
||||||
|
<at-row ng-repeat="user in users">
|
||||||
|
<div class="at-Row-items">
|
||||||
|
<at-row-item
|
||||||
|
header-value="{{ user.summary_fields.user.username }}"
|
||||||
|
header-link="/#/users/{{ user.summary_fields.user.id }}">
|
||||||
|
</at-row-item>
|
||||||
|
<at-row-item
|
||||||
|
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_DESCRIPTION') }}"
|
||||||
|
value="{{ user.description }}">
|
||||||
|
</at-row-item>
|
||||||
|
<at-row-item
|
||||||
|
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_ORGANIZATION') }}"
|
||||||
|
value="{{ user.summary_fields.organization.name }}"
|
||||||
|
value-link="/#/organizations/{{ user.summary_fields.organization.id }}">
|
||||||
|
</at-row-item>
|
||||||
|
<at-row-item
|
||||||
|
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_EXPIRED') }}"
|
||||||
|
value="{{ user.expriation | longDate }}">
|
||||||
|
</at-row-item>
|
||||||
|
<at-row-item
|
||||||
|
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_MODIFIED') }}"
|
||||||
|
value="{{ vm.getLastUsed(user) }}">
|
||||||
|
</at-row-item>
|
||||||
|
</div>
|
||||||
|
</at-row>
|
||||||
|
</at-list>
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
/** ***********************************************
|
||||||
|
* Copyright (c) 2018 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
************************************************ */
|
||||||
|
function ListApplicationsController (
|
||||||
|
$filter,
|
||||||
|
$scope,
|
||||||
|
$state,
|
||||||
|
Dataset,
|
||||||
|
ProcessErrors,
|
||||||
|
Prompt,
|
||||||
|
resolvedModels,
|
||||||
|
strings,
|
||||||
|
Wait,
|
||||||
|
) {
|
||||||
|
const vm = this || {};
|
||||||
|
const application = resolvedModels;
|
||||||
|
|
||||||
|
vm.strings = strings;
|
||||||
|
vm.activeId = $state.params.application_id;
|
||||||
|
|
||||||
|
$scope.canAdd = application.options('actions.POST');
|
||||||
|
|
||||||
|
// smart-search
|
||||||
|
const name = 'applications';
|
||||||
|
const iterator = 'application';
|
||||||
|
const key = 'application_dataset';
|
||||||
|
|
||||||
|
$scope.list = { iterator, name, basePath: 'applications' };
|
||||||
|
$scope.collection = { iterator };
|
||||||
|
$scope[key] = Dataset.data;
|
||||||
|
vm.applicationsCount = Dataset.data.count;
|
||||||
|
$scope[name] = Dataset.data.results;
|
||||||
|
$scope.$on('updateDataset', (e, dataset) => {
|
||||||
|
$scope[key] = dataset;
|
||||||
|
$scope[name] = dataset.results;
|
||||||
|
vm.applicationsCount = dataset.count;
|
||||||
|
});
|
||||||
|
|
||||||
|
vm.getModified = app => {
|
||||||
|
const modified = _.get(app, 'modified');
|
||||||
|
|
||||||
|
if (!modified) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = $filter('longDate')(modified);
|
||||||
|
|
||||||
|
const { username, id } = _.get(app, 'summary_fields.modified_by', {});
|
||||||
|
|
||||||
|
if (username && id) {
|
||||||
|
html += ` by <a href="/#/users/${id}">${$filter('sanitize')(username)}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.deleteApplication = (app) => {
|
||||||
|
const action = () => {
|
||||||
|
$('#prompt-modal').modal('hide');
|
||||||
|
Wait('start');
|
||||||
|
application.request('delete', app.id)
|
||||||
|
.then(() => {
|
||||||
|
let reloadListStateParams = null;
|
||||||
|
|
||||||
|
if ($scope.applications.length === 1 && $state.params.application_search &&
|
||||||
|
!_.isEmpty($state.params.application_search.page) &&
|
||||||
|
$state.params.application_search.page !== '1') {
|
||||||
|
const page = `${(parseInt(reloadListStateParams
|
||||||
|
.application_search.page, 10) - 1)}`;
|
||||||
|
reloadListStateParams = _.cloneDeep($state.params);
|
||||||
|
reloadListStateParams.application_search.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parseInt($state.params.application_id, 10) === app.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: `${application.path}${app.id}`, status })
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
Wait('stop');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteModalBody = `<div class="Prompt-bodyQuery">${strings.get('deleteResource.CONFIRM', 'application')}</div>`;
|
||||||
|
|
||||||
|
Prompt({
|
||||||
|
hdr: strings.get('deleteResource.HEADER'),
|
||||||
|
resourceName: $filter('sanitize')(app.name),
|
||||||
|
body: deleteModalBody,
|
||||||
|
action,
|
||||||
|
actionText: 'DELETE'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ListApplicationsController.$inject = [
|
||||||
|
'$filter',
|
||||||
|
'$scope',
|
||||||
|
'$state',
|
||||||
|
'Dataset',
|
||||||
|
'ProcessErrors',
|
||||||
|
'Prompt',
|
||||||
|
'resolvedModels',
|
||||||
|
'ApplicationsStrings',
|
||||||
|
'Wait'
|
||||||
|
];
|
||||||
|
|
||||||
|
export default ListApplicationsController;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<at-panel>
|
||||||
|
<at-panel-heading hide-dismiss="true">
|
||||||
|
APPLICATIONS
|
||||||
|
<span class="badge List-titleBadge">
|
||||||
|
{{ vm.applicationsCount }}
|
||||||
|
</span>
|
||||||
|
</at-panel-heading>
|
||||||
|
|
||||||
|
<at-panel-body>
|
||||||
|
<div class="at-List-toolbar">
|
||||||
|
<smart-search
|
||||||
|
class="at-List-search"
|
||||||
|
django-model="applications"
|
||||||
|
base-path="applications"
|
||||||
|
iterator="application"
|
||||||
|
list="list"
|
||||||
|
dataset="application_dataset"
|
||||||
|
collection="collection"
|
||||||
|
search-tags="searchTags">
|
||||||
|
</smart-search>
|
||||||
|
<div class="at-List-toolbarAction">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
ui-sref="applications.add"
|
||||||
|
class="at-Button--add"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="false">
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<at-list results="applications">
|
||||||
|
<at-row ng-repeat="application in applications"
|
||||||
|
ng-class="{'at-Row--active': (application.id === vm.activeId)}">
|
||||||
|
<div class="at-Row-items">
|
||||||
|
<at-row-item
|
||||||
|
header-value="{{ application.name }}"
|
||||||
|
header-link="/#/applications/{{ application.id }}">
|
||||||
|
</at-row-item>
|
||||||
|
<at-row-item
|
||||||
|
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_ORGANIZATION') }}"
|
||||||
|
value="{{ application.summary_fields.organization.name }}"
|
||||||
|
value-link="/#/organizations/{{ application.summary_fields.organization.id }}">
|
||||||
|
</at-row-item>
|
||||||
|
<at-row-item
|
||||||
|
label-value="{{:: vm.strings.get('list.ROW_ITEM_LABEL_MODIFIED') }}"
|
||||||
|
value="{{ vm.getModified(application) }}">
|
||||||
|
</at-row-item>
|
||||||
|
</div>
|
||||||
|
<div class="at-Row-actions">
|
||||||
|
<!-- TODO: delete application should be based on user capabilities but that's not
|
||||||
|
implemented in the api yet -->
|
||||||
|
<at-row-action icon="fa-trash" ng-click="vm.deleteApplication(application)">
|
||||||
|
</at-row-action>
|
||||||
|
<!-- <at-row-action icon="fa-trash" ng-click="vm.deleteApplication(application)"
|
||||||
|
ng-show="application.summary_fields.user_capabilities.delete">
|
||||||
|
</at-row-action> -->
|
||||||
|
</div>
|
||||||
|
</at-row>
|
||||||
|
</at-list>
|
||||||
|
</at-panel-body>
|
||||||
|
<paginate
|
||||||
|
base-path="applications"
|
||||||
|
iterator="application"
|
||||||
|
dataset="application_dataset"
|
||||||
|
collection="applications"
|
||||||
|
query-set="application_queryset">
|
||||||
|
</paginate>
|
||||||
|
</at-panel>
|
||||||
@@ -2,6 +2,7 @@ import atLibServices from '~services';
|
|||||||
import atLibComponents from '~components';
|
import atLibComponents from '~components';
|
||||||
import atLibModels from '~models';
|
import atLibModels from '~models';
|
||||||
|
|
||||||
|
import atFeaturesApplications from '~features/applications';
|
||||||
import atFeaturesCredentials from '~features/credentials';
|
import atFeaturesCredentials from '~features/credentials';
|
||||||
import atFeaturesTemplates from '~features/templates';
|
import atFeaturesTemplates from '~features/templates';
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ angular.module(MODULE_NAME, [
|
|||||||
atLibServices,
|
atLibServices,
|
||||||
atLibComponents,
|
atLibComponents,
|
||||||
atLibModels,
|
atLibModels,
|
||||||
|
atFeaturesApplications,
|
||||||
atFeaturesCredentials,
|
atFeaturesCredentials,
|
||||||
atFeaturesTemplates
|
atFeaturesTemplates
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ function ComponentsStrings (BaseString) {
|
|||||||
MANAGEMENT_JOBS: t.s('Management Jobs'),
|
MANAGEMENT_JOBS: t.s('Management Jobs'),
|
||||||
INSTANCES: t.s('Instances'),
|
INSTANCES: t.s('Instances'),
|
||||||
INSTANCE_GROUPS: t.s('Instance Groups'),
|
INSTANCE_GROUPS: t.s('Instance Groups'),
|
||||||
|
APPLICATIONS: t.s('Applications'),
|
||||||
SETTINGS: t.s('Settings'),
|
SETTINGS: t.s('Settings'),
|
||||||
FOOTER_ABOUT: t.s('About'),
|
FOOTER_ABOUT: t.s('About'),
|
||||||
FOOTER_COPYRIGHT: t.s('Copyright © 2017 Red Hat, Inc.')
|
FOOTER_COPYRIGHT: t.s('Copyright © 2017 Red Hat, Inc.')
|
||||||
|
|||||||
@@ -110,6 +110,10 @@
|
|||||||
padding: @at-padding-side-nav-item-icon;
|
padding: @at-padding-side-nav-item-icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.fa-cubes {
|
||||||
|
margin-left: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&.is-active {
|
&.is-active {
|
||||||
background: @at-color-side-nav-item-background-hover;
|
background: @at-color-side-nav-item-background-hover;
|
||||||
@@ -119,12 +123,16 @@
|
|||||||
color: @at-color-side-nav-content;
|
color: @at-color-side-nav-content;
|
||||||
margin-left: @at-highlight-left-border-margin-makeup;
|
margin-left: @at-highlight-left-border-margin-makeup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.fa-cubes {
|
||||||
|
margin-left: -9px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.at-Layout-sideNavSpacer {
|
.at-Layout-sideNavSpacer {
|
||||||
background: inherit;
|
background: inherit;
|
||||||
height: @at-height-side-nav-spacer;
|
height: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--expanded {
|
&--expanded {
|
||||||
|
|||||||
@@ -71,6 +71,9 @@
|
|||||||
system-admin-only="true">
|
system-admin-only="true">
|
||||||
</at-side-nav-item>
|
</at-side-nav-item>
|
||||||
<div class="at-Layout-sideNavSpacer"></div>
|
<div class="at-Layout-sideNavSpacer"></div>
|
||||||
|
<at-side-nav-item icon-class="fa-cubes" route="applications" name="APPLICATIONS"
|
||||||
|
system-admin-only="true">
|
||||||
|
</at-side-nav-item>
|
||||||
<at-side-nav-item icon-class="fa-cog" route="configuration" name="SETTINGS"
|
<at-side-nav-item icon-class="fa-cog" route="configuration" name="SETTINGS"
|
||||||
system-admin-only="true">
|
system-admin-only="true">
|
||||||
</at-side-nav-item>
|
</at-side-nav-item>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
const templateUrl = require('~components/list/list.partial.html');
|
const templateUrl = require('~components/list/list.partial.html');
|
||||||
|
|
||||||
// TODO: figure out emptyListReason scope property
|
function atListLink (scope, element, attrs) {
|
||||||
|
if (!attrs.results) {
|
||||||
|
throw new Error('at-list directive requires results attr to set up the empty list properly');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function AtListController (strings) {
|
function AtListController (strings) {
|
||||||
this.strings = strings;
|
this.strings = strings;
|
||||||
@@ -17,6 +21,7 @@ function atList () {
|
|||||||
scope: {
|
scope: {
|
||||||
results: '=',
|
results: '=',
|
||||||
},
|
},
|
||||||
|
link: atListLink,
|
||||||
controller: AtListController,
|
controller: AtListController,
|
||||||
controllerAs: 'vm',
|
controllerAs: 'vm',
|
||||||
};
|
};
|
||||||
|
|||||||
51
awx/ui/client/lib/models/Application.js
Normal file
51
awx/ui/client/lib/models/Application.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
let Base;
|
||||||
|
|
||||||
|
function createFormSchema (method, config) {
|
||||||
|
if (!config) {
|
||||||
|
config = method;
|
||||||
|
method = 'GET';
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = Object.assign({}, this.options(`actions.${method.toUpperCase()}`));
|
||||||
|
|
||||||
|
if (config && config.omit) {
|
||||||
|
config.omit.forEach(key => delete schema[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(schema).forEach(key => {
|
||||||
|
schema[key].id = key;
|
||||||
|
|
||||||
|
if (this.has(key)) {
|
||||||
|
schema[key]._value = this.get(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDependentResources () {
|
||||||
|
this.dependentResources = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApplicationModel (method, resource, config) {
|
||||||
|
// TODO: change to applications
|
||||||
|
Base.call(this, 'applications');
|
||||||
|
|
||||||
|
this.Constructor = ApplicationModel;
|
||||||
|
this.createFormSchema = createFormSchema.bind(this);
|
||||||
|
this.setDependentResources = setDependentResources.bind(this);
|
||||||
|
|
||||||
|
return this.create(method, resource, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApplicationModelLoader (BaseModel) {
|
||||||
|
Base = BaseModel;
|
||||||
|
|
||||||
|
return ApplicationModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationModelLoader.$inject = [
|
||||||
|
'BaseModel',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default ApplicationModelLoader;
|
||||||
@@ -361,7 +361,11 @@ function normalizePath (resource) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isEditable () {
|
function isEditable () {
|
||||||
const canEdit = this.get('summary_fields.user_capabilities.edit');
|
let canEdit = this.get('summary_fields.user_capabilities.edit');
|
||||||
|
|
||||||
|
if (canEdit === undefined) {
|
||||||
|
canEdit = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (canEdit) {
|
if (canEdit) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import atLibServices from '~services';
|
import atLibServices from '~services';
|
||||||
|
|
||||||
|
import Application from '~models/Application';
|
||||||
import Base from '~models/Base';
|
import Base from '~models/Base';
|
||||||
import Config from '~models/Config';
|
import Config from '~models/Config';
|
||||||
import Credential from '~models/Credential';
|
import Credential from '~models/Credential';
|
||||||
@@ -28,6 +29,7 @@ angular
|
|||||||
.module(MODULE_NAME, [
|
.module(MODULE_NAME, [
|
||||||
atLibServices
|
atLibServices
|
||||||
])
|
])
|
||||||
|
.service('ApplicationModel', Application)
|
||||||
.service('BaseModel', Base)
|
.service('BaseModel', Base)
|
||||||
.service('ConfigModel', Config)
|
.service('ConfigModel', Config)
|
||||||
.service('CredentialModel', Credential)
|
.service('CredentialModel', Credential)
|
||||||
|
|||||||
@@ -316,8 +316,23 @@ angular
|
|||||||
activateTab();
|
activateTab();
|
||||||
});
|
});
|
||||||
|
|
||||||
$transitions.onSuccess({}, function(trans) {
|
$transitions.onCreate({}, function(trans) {
|
||||||
|
console.log('$onCreate ' +trans.to().name);
|
||||||
|
});
|
||||||
|
|
||||||
|
$transitions.onBefore({}, function(trans) {
|
||||||
|
console.log('$onBefore ' +trans.to().name);
|
||||||
|
});
|
||||||
|
$transitions.onError({}, function(trans) {
|
||||||
|
|
||||||
|
console.log('$onError ' +trans.to().name);
|
||||||
|
});
|
||||||
|
$transitions.onExit({}, function(trans) {
|
||||||
|
console.log('$onExit ' +trans.to().name);
|
||||||
|
});
|
||||||
|
|
||||||
|
$transitions.onSuccess({}, function(trans) {
|
||||||
|
console.log('$onSuccess ' +trans.to().name);
|
||||||
if(trans.to() === trans.from()) {
|
if(trans.to() === trans.from()) {
|
||||||
// check to see if something other than a search param has changed
|
// check to see if something other than a search param has changed
|
||||||
let toParamsWithoutSearchKeys = {};
|
let toParamsWithoutSearchKeys = {};
|
||||||
|
|||||||
Reference in New Issue
Block a user