diff --git a/awx/ui/client/features/applications/add-applications.controller.js b/awx/ui/client/features/applications/add-applications.controller.js new file mode 100644 index 0000000000..173460157e --- /dev/null +++ b/awx/ui/client/features/applications/add-applications.controller.js @@ -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; diff --git a/awx/ui/client/features/applications/add-edit-applications.view.html b/awx/ui/client/features/applications/add-edit-applications.view.html new file mode 100644 index 0000000000..a8d8d68e6c --- /dev/null +++ b/awx/ui/client/features/applications/add-edit-applications.view.html @@ -0,0 +1,29 @@ + + + {{ vm.panelTitle }} + + + + {{:: vm.strings.get('tab.DETAILS') }} + {{:: vm.strings.get('tab.USERS') }} + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/awx/ui/client/features/applications/applications.strings.js b/awx/ui/client/features/applications/applications.strings.js new file mode 100644 index 0000000000..18158c4d4e --- /dev/null +++ b/awx/ui/client/features/applications/applications.strings.js @@ -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; diff --git a/awx/ui/client/features/applications/edit-applications.controller.js b/awx/ui/client/features/applications/edit-applications.controller.js new file mode 100644 index 0000000000..6279b642ee --- /dev/null +++ b/awx/ui/client/features/applications/edit-applications.controller.js @@ -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; diff --git a/awx/ui/client/features/applications/index.js b/awx/ui/client/features/applications/index.js new file mode 100644 index 0000000000..ca70be6165 --- /dev/null +++ b/awx/ui/client/features/applications/index.js @@ -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 `${html}`; + } + } + }, + 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 `${html}`; + } + } + }, + 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; diff --git a/awx/ui/client/features/applications/index.view.html b/awx/ui/client/features/applications/index.view.html new file mode 100644 index 0000000000..b4135fb791 --- /dev/null +++ b/awx/ui/client/features/applications/index.view.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/awx/ui/client/features/applications/list-applications-users.controller.js b/awx/ui/client/features/applications/list-applications-users.controller.js new file mode 100644 index 0000000000..1c6856498b --- /dev/null +++ b/awx/ui/client/features/applications/list-applications-users.controller.js @@ -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 ${$filter('sanitize')(username)}`; + } + + return html; + }; +} + +ListApplicationsUsersController.$inject = [ + '$filter', + '$scope', + 'Dataset', + 'resolvedModels', + 'ApplicationsStrings' +]; + +export default ListApplicationsUsersController; diff --git a/awx/ui/client/features/applications/list-applications-users.view.html b/awx/ui/client/features/applications/list-applications-users.view.html new file mode 100644 index 0000000000..3acb66242d --- /dev/null +++ b/awx/ui/client/features/applications/list-applications-users.view.html @@ -0,0 +1,40 @@ +
+ + +
+ + + +
+ + + + + + + + + + +
+
+
diff --git a/awx/ui/client/features/applications/list-applications.controller.js b/awx/ui/client/features/applications/list-applications.controller.js new file mode 100644 index 0000000000..fd67589a08 --- /dev/null +++ b/awx/ui/client/features/applications/list-applications.controller.js @@ -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 ${$filter('sanitize')(username)}`; + } + + 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 = `
${strings.get('deleteResource.CONFIRM', 'application')}
`; + + 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; diff --git a/awx/ui/client/features/applications/list-applications.view.html b/awx/ui/client/features/applications/list-applications.view.html new file mode 100644 index 0000000000..1803e5ab66 --- /dev/null +++ b/awx/ui/client/features/applications/list-applications.view.html @@ -0,0 +1,69 @@ + + + APPLICATIONS + + {{ vm.applicationsCount }} + + + + +
+ + +
+ +
+
+ + + +
+ + + + + + +
+
+ + + + +
+
+
+
+ + +
diff --git a/awx/ui/client/features/index.js b/awx/ui/client/features/index.js index 1cb67b73d4..75093bf39c 100644 --- a/awx/ui/client/features/index.js +++ b/awx/ui/client/features/index.js @@ -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 ]); diff --git a/awx/ui/client/lib/components/components.strings.js b/awx/ui/client/lib/components/components.strings.js index a827ee890c..70c2a7d6de 100644 --- a/awx/ui/client/lib/components/components.strings.js +++ b/awx/ui/client/lib/components/components.strings.js @@ -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.') diff --git a/awx/ui/client/lib/components/layout/_index.less b/awx/ui/client/lib/components/layout/_index.less index 96159da01e..fd4e29c144 100644 --- a/awx/ui/client/lib/components/layout/_index.less +++ b/awx/ui/client/lib/components/layout/_index.less @@ -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 @@ } } } -} \ No newline at end of file +} diff --git a/awx/ui/client/lib/components/layout/layout.partial.html b/awx/ui/client/lib/components/layout/layout.partial.html index 0ffa4a1ad1..b1ada5e000 100644 --- a/awx/ui/client/lib/components/layout/layout.partial.html +++ b/awx/ui/client/lib/components/layout/layout.partial.html @@ -71,6 +71,9 @@ system-admin-only="true">
+ + diff --git a/awx/ui/client/lib/components/list/list.directive.js b/awx/ui/client/lib/components/list/list.directive.js index 28c3c546ea..6ff88e506f 100644 --- a/awx/ui/client/lib/components/list/list.directive.js +++ b/awx/ui/client/lib/components/list/list.directive.js @@ -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', }; diff --git a/awx/ui/client/lib/models/Application.js b/awx/ui/client/lib/models/Application.js new file mode 100644 index 0000000000..5c8cdbd066 --- /dev/null +++ b/awx/ui/client/lib/models/Application.js @@ -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; diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js index 01392eafb9..a2c202aa79 100644 --- a/awx/ui/client/lib/models/Base.js +++ b/awx/ui/client/lib/models/Base.js @@ -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; diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index fdecf596fd..9f3e57b3cd 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -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) diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index 0fd75d8280..ab93141b7d 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -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 = {};