add app crud ui

This commit is contained in:
John Mitchell
2018-03-05 12:01:23 -05:00
parent c19bb79587
commit f4ad9afc5e
22 changed files with 1311 additions and 5 deletions

View File

@@ -0,0 +1,79 @@
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 },
permissions: { _disabled: 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;

View File

@@ -0,0 +1,36 @@
<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.permissions">{{:: vm.strings.get('tab.PERMISSIONS') }}</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('permissions') || $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('permissions')">
<div ui-view="related"></div>
</at-panel-body>
<at-panel-body ng-if="$state.current.name.includes('users')">
<div ui-view="userList"></div>
</at-panel-body>
</at-panel>
<div ng-if="$state.current.name.includes('permissions.add')" ui-view="modal"></div>

View File

@@ -0,0 +1,34 @@
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('USERS'),
PERMISSIONS_BREADCRUMB_LABEL: t.s('PERMISSIONS')
};
ns.tab = {
DETAILS: t.s('Details'),
PERMISSIONS: t.s('Permissions'),
USERS: t.s('Users')
};
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;

View File

@@ -0,0 +1,124 @@
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') }
},
permissions: {
_go: 'applications.edit.permissions',
_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.permissions._active = false;
vm.tab.users._active = true;
} else if (/applications.edit($|\.organization$)/.test(value)) {
vm.tab.details._active = true;
vm.tab.permissions._active = false;
vm.tab.users._active = false;
} else {
vm.tab.details._active = false;
vm.tab.permissions._active = true;
vm.tab.users._active = false;
}
});
// Only exists for permissions compatibility
$scope.application_obj = application.get();
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.add.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('post', { data: payload });
};
}
EditApplicationsController.$inject = [
'resolvedModels',
'$state',
'ApplicationsStrings',
'$scope'
];
export default EditApplicationsController;

View File

@@ -0,0 +1,437 @@
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';
import { N_ } from '../../src/i18n';
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.permissions',
route: '/permissions?{permission_search:queryset}',
ncyBreadcrumb: {
label: strings.get('state.PERMISSIONS_BREADCRUMB_LABEL'),
parent: 'applications.edit'
},
params: {
permission_search: {
dynamic: true,
squash: '',
value: {
page_size: '20',
order_by: 'username'
}
}
},
resolve: {
Dataset: ['QuerySet', '$stateParams', (qs, $stateParams) => {
const id = $stateParams.application_id;
// TODO: no access_list endpoint given by api
const path = `api/v2/applications/${id}/access_list/`;
return qs.search(path, $stateParams.permission_search);
}],
ListDefinition: () => ({
name: 'permissions',
disabled: 'organization === undefined',
ngClick: 'organization === undefined || $state.go(\'applications.edit.permissions\')',
awToolTip: '{{permissionsTooltip}}',
dataTipWatch: 'permissionsTooltip',
awToolTipTabEnabledInEditMode: true,
dataPlacement: 'right',
basePath: 'api/v2/applications/{{$stateParams.id}}/access_list/',
search: {
order_by: 'username'
},
type: 'collection',
title: N_('Permissions'),
iterator: 'permission',
index: false,
open: false,
actions: {
add: {
ngClick: '$state.go(\'.add\')',
label: 'Add',
awToolTip: N_('Add a permission'),
actionClass: 'btn List-buttonSubmit',
buttonContent: `&#43; ${N_('ADD')}`,
ngShow: '(application_obj.summary_fields.user_capabilities.edit || canAdd)'
}
},
fields: {
username: {
key: true,
label: N_('User'),
linkBase: 'users',
class: 'col-lg-3 col-md-3 col-sm-3 col-xs-4'
},
role: {
label: N_('Role'),
type: 'role',
nosort: true,
class: 'col-lg-4 col-md-4 col-sm-4 col-xs-4'
},
team_roles: {
label: N_('Team Roles'),
type: 'team_roles',
nosort: true,
class: 'col-lg-5 col-md-5 col-sm-5 col-xs-4'
}
}
}),
}
});
$stateExtender.addState({
name: 'applications.edit.permissions.add',
url: '/add-permissions',
resolve: {
usersDataset: [
'addPermissionsUsersList',
'QuerySet',
'$stateParams',
'GetBasePath',
'resourceData',
(list, qs, $stateParams, GetBasePath, resourceData) => {
let path;
if (resourceData.data.organization) {
path = `${GetBasePath('organizations')}${resourceData.data.organization}/users`;
} else {
path = list.basePath || GetBasePath(list.name);
}
return qs.search(path, $stateParams.user_search);
}
],
teamsDataset: [
'addPermissionsTeamsList',
'QuerySet',
'$stateParams',
'GetBasePath',
'resourceData',
(list, qs, $stateParams, GetBasePath, resourceData) => {
const path = GetBasePath(list.basePath) || GetBasePath(list.name);
const org = resourceData.data.organization;
if (!org) {
return null;
}
$stateParams[`${list.iterator}_search`].organization = org;
return qs.search(path, $stateParams.team_search);
}
],
resourceData: ['ApplicationModel', '$stateParams', (Application, $stateParams) =>
new Application('get', $stateParams.application_id)
.then(application => ({ data: application.get() }))
]
},
params: {
user_search: {
value: {
order_by: 'username',
page_size: 5,
is_superuser: false
},
dynamic: true
},
team_search: {
value: {
order_by: 'name',
page_size: 5
},
dynamic: true
}
},
ncyBreadcrumb: {
skip: true
},
views: {
'modal@applications.edit': {
template: `
<add-rbac-resource
users-dataset='$resolve.usersDataset'
teams-dataset='$resolve.teamsDataset'
selected='allSelected'
resource-data='$resolve.resourceData'
without-team-permissions='{{$resolve.resourceData.data.organization ? null : true}}'
title='{{$resolve.resourceData.data.organization ? "Add Users / Teams" : "Add Users"}}'>
</add-rbac-resource>`
}
},
onExit: $state => {
if ($state.transition) {
$('#add-permissions-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;

View File

@@ -0,0 +1,3 @@
<div ui-view="edit"></div>
<div ui-view="add"></div>
<div ui-view="list"></div>

View File

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

View File

@@ -0,0 +1,36 @@
<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="users in users">
<div class="at-Row-items">
<at-row-item
header-value="{{ user.name }}"
header-link="/#/users/{{ user.id }}">
</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>

View File

@@ -0,0 +1,146 @@
/** ***********************************************
* Copyright (c) 2018 Ansible, Inc.
*
* All Rights Reserved
************************************************ */
function ListApplicationsController (
$filter,
$scope,
$state,
Alert,
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 => {
if (!app) {
Alert(strings.get('error.DELETE'), strings.get('alert.MISSING_PARAMETER'));
return;
}
application.getDependentResourceCounts(application.id)
.then(counts => displayApplicationDeletePrompt(app, counts));
};
function createErrorHandler (path, action) {
return ({ data, status }) => {
const hdr = strings.get('error.HEADER');
const msg = strings.get('error.CALL', { path, action, status });
ProcessErrors($scope, data, status, null, { hdr, msg });
};
}
function handleSuccessfulDelete (app) {
const { page } = _.get($state.params, 'application_search');
let reloadListStateParams = null;
if ($scope.applications.length === 1 && !_.isEmpty(page) && page !== '1') {
reloadListStateParams = _.cloneDeep($state.params);
const pageNumber = (parseInt(reloadListStateParams.application_search.page, 0) - 1);
reloadListStateParams.application_search.page = pageNumber.toString();
}
if (parseInt($state.params.application_id, 0) === app.id) {
$state.go('applications', reloadListStateParams, { reload: true });
} else if (parseInt($state.params.application_id, 0) === application.id) {
$state.go('applications', reloadListStateParams, { reload: true });
} else {
$state.go('.', reloadListStateParams, { reload: true });
}
}
function displayApplicationDeletePrompt (app, counts) {
Prompt({
action () {
$('#prompt-modal').modal('hide');
Wait('start');
application
.request('delete', app.id)
.then(() => handleSuccessfulDelete(app))
.catch(createErrorHandler('delete application', 'DELETE'))
.finally(() => Wait('stop'));
},
hdr: strings.get('DELETE'),
resourceName: $filter('sanitize')(app.name),
body: buildApplicationDeletePromptHTML(counts),
});
}
function buildApplicationDeletePromptHTML (counts) {
const buildCount = count => `<span class="badge List-titleBadge">${count}</span>`;
const buildLabel = label => `<span class="Prompt-warningResourceTitle">
${$filter('sanitize')(label)}</span>`;
const buildCountLabel = ({ count, label }) => `<div>
${buildLabel(label)}${buildCount(count)}</div>`;
const displayedCounts = counts.filter(({ count }) => count > 0);
const html = `
${displayedCounts.length ? strings.get('deleteResource.USED_BY', 'application') : ''}
${strings.get('deleteResource.CONFIRM', 'application')}
${displayedCounts.map(buildCountLabel).join('')}
`;
return html;
}
}
ListApplicationsController.$inject = [
'$filter',
'$scope',
'$state',
'Alert',
'Dataset',
'ProcessErrors',
'Prompt',
'resolvedModels',
'ApplicationsStrings',
'Wait'
];
export default ListApplicationsController;

View File

@@ -0,0 +1,65 @@
<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">
<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>

View File

@@ -2,6 +2,7 @@ import atLibServices from '~services';
import atLibComponents from '~components';
import atLibModels from '~models';
import atFeaturesApplications from '~features/applications';
import atFeaturesCredentials from '~features/credentials';
import atFeaturesTemplates from '~features/templates';
@@ -11,6 +12,7 @@ angular.module(MODULE_NAME, [
atLibServices,
atLibComponents,
atLibModels,
atFeaturesApplications,
atFeaturesCredentials,
atFeaturesTemplates
]);

View File

@@ -73,6 +73,7 @@ function ComponentsStrings (BaseString) {
MANAGEMENT_JOBS: t.s('Management Jobs'),
INSTANCES: t.s('Instances'),
INSTANCE_GROUPS: t.s('Instance Groups'),
APPLICATIONS: t.s('Applications'),
SETTINGS: t.s('Settings'),
FOOTER_ABOUT: t.s('About'),
FOOTER_COPYRIGHT: t.s('Copyright © 2017 Red Hat, Inc.')

View File

@@ -110,6 +110,10 @@
padding: @at-padding-side-nav-item-icon;
}
i.fa-cubes {
margin-left: -4px;
}
&:hover,
&.is-active {
background: @at-color-side-nav-item-background-hover;
@@ -119,12 +123,16 @@
color: @at-color-side-nav-content;
margin-left: @at-highlight-left-border-margin-makeup;
}
i.fa-cubes {
margin-left: -9px;
}
}
}
.at-Layout-sideNavSpacer {
background: inherit;
height: @at-height-side-nav-spacer;
height: 5px;
}
&--expanded {
@@ -213,4 +221,4 @@
}
}
}
}
}

View File

@@ -71,6 +71,9 @@
system-admin-only="true">
</at-side-nav-item>
<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"
system-admin-only="true">
</at-side-nav-item>

View File

@@ -1,6 +1,10 @@
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) {
this.strings = strings;
@@ -17,6 +21,7 @@ function atList () {
scope: {
results: '=',
},
link: atListLink,
controller: AtListController,
controllerAs: 'vm',
};

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

View File

@@ -361,7 +361,11 @@ function normalizePath (resource) {
}
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) {
return true;

View File

@@ -1,5 +1,6 @@
import atLibServices from '~services';
import Application from '~models/Application';
import Base from '~models/Base';
import Config from '~models/Config';
import Credential from '~models/Credential';
@@ -28,6 +29,7 @@ angular
.module(MODULE_NAME, [
atLibServices
])
.service('ApplicationModel', Application)
.service('BaseModel', Base)
.service('ConfigModel', Config)
.service('CredentialModel', Credential)

View File

@@ -316,8 +316,23 @@ angular
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()) {
// check to see if something other than a search param has changed
let toParamsWithoutSearchKeys = {};

View File

@@ -0,0 +1,22 @@
export default ['i18n', function(i18n) {
return {
name: 'applications',
search: {
order_by: 'id'
},
iterator: 'application',
// TODO: change
basePath: 'projects',
listTitle: i18n._('APPLICATIONS'),
index: false,
hover: true,
fields: {
name: {
key: true,
label: i18n._('Name'),
columnClass: 'col-md-3 col-sm-3 col-xs-9'
},
}
};}];

View File

@@ -0,0 +1,108 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
export default ['Rest', 'Wait', 'UserTokensFormObject',
'ProcessErrors', 'GetBasePath', 'Alert',
'GenerateForm', '$scope', '$state', 'CreateSelect2', 'GetChoices' 'i18n',
function(
Rest, Wait, UserTokensFormObject,
ProcessErrors, GetBasePath, Alert,
GenerateForm, $scope, $state, CreateSelect2, GetChoices i18n
) {
var generator = GenerateForm,
form = UserTokensFormObject;
init();
function init() {
Rest.setUrl(GetBasePath('users') + '/authorized_tokens');
Rest.options()
.then(({data}) => {
if (!data.actions.POST) {
$state.go("^");
Alert('Permission Error', 'You do not have permission to add a token.', 'alert-info');
}
});
// apply form definition's default field values
GenerateForm.applyDefaults(form, $scope);
}
CreateSelect2({
element: '#user_token_scope',
multiple: false
});
// Save
$scope.formSave = function() {
// var params,
// v = $scope.notification_type.value;
//
// generator.clearApiErrors($scope);
// params = {
// "name": $scope.name,
// "description": $scope.description,
// "organization": $scope.organization,
// "notification_type": v,
// "notification_configuration": {}
// };
//
// function processValue(value, i, field) {
// if (field.type === 'textarea') {
// if (field.name === 'headers') {
// $scope[i] = JSON.parse($scope[i]);
// } else {
// $scope[i] = $scope[i].toString().split('\n');
// }
// }
// if (field.type === 'checkbox') {
// $scope[i] = Boolean($scope[i]);
// }
// if (field.type === 'number') {
// $scope[i] = Number($scope[i]);
// }
// if (i === "username" && $scope.notification_type.value === "email" && value === null) {
// $scope[i] = "";
// }
// if (field.type === 'sensitive' && value === null) {
// $scope[i] = "";
// }
// return $scope[i];
// }
//
// params.notification_configuration = _.object(Object.keys(form.fields)
// .filter(i => (form.fields[i].ngShow && form.fields[i].ngShow.indexOf(v) > -1))
// .map(i => [i, processValue($scope[i], i, form.fields[i])]));
//
// delete params.notification_configuration.email_options;
//
// for(var j = 0; j < form.fields.email_options.options.length; j++) {
// if(form.fields.email_options.options[j].ngShow && form.fields.email_options.options[j].ngShow.indexOf(v) > -1) {
// params.notification_configuration[form.fields.email_options.options[j].value] = Boolean($scope[form.fields.email_options.options[j].value]);
// }
// }
//
// Wait('start');
// Rest.setUrl(url);
// Rest.post(params)
// .then(() => {
// $state.go('notifications', {}, { reload: true });
// Wait('stop');
// })
// .catch(({data, status}) => {
// ProcessErrors($scope, data, status, form, {
// hdr: 'Error!',
// msg: 'Failed to add new notifier. POST returned status: ' + status
// });
// });
};
$scope.formCancel = function() {
$state.go('notifications');
};
}
];

View File

@@ -0,0 +1,70 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name forms.function:Tokens
* @description This form is for adding a token on the user's page
*/
export default ['i18n',
function(i18n) {
return {
addTitle: i18n._('CREATE TOKEN'),
name: 'token',
basePath: 'tokens',
well: false,
formLabelSize: 'col-lg-3',
formFieldSize: 'col-lg-9',
iterator: 'token',
stateTree: 'users',
fields: {
application: {
label: i18n._('Application'),
type: 'lookup',
list: 'ApplicationList',
sourceModel: 'application',
// TODO: update to actual path
basePath: 'projects',
sourceField: 'name',
dataTitle: i18n._('Application'),
required: true,
dataContainer: 'body',
dataPlacement: 'right',
ngDisabled: '!(token.summary_fields.user_capabilities.edit || canAdd)',
awLookupWhen: '(token.summary_fields.user_capabilities.edit || canAdd)'
// TODO: help popover
},
scope: {
label: i18n._('Description'),
type: 'select',
class: 'Form-dropDown--scmType',
defaultText: 'Choose a scope',
ngOptions: 'scope.label for scope in scope_options track by scope.value',
required: true,
ngDisabled: '!(token.summary_fields.user_capabilities.edit || canAdd)'
// TODO: help popover
}
},
buttons: {
cancel: {
ngClick: 'formCancel()',
ngShow: '(token.summary_fields.user_capabilities.edit || canAdd)'
},
close: {
ngClick: 'formCancel()',
ngShow: '!(token.summary_fields.user_capabilities.edit || canAdd)'
},
save: {
ngClick: 'formSave()',
ngDisabled: true,
ngShow: '(token.summary_fields.user_capabilities.edit || canAdd)'
}
},
related: {
}
};
}];