From bdb2bbbd411c2128bd9560c4a71fa80d7c1af070 Mon Sep 17 00:00:00 2001
From: gconsidine
Date: Mon, 31 Jul 2017 17:07:25 -0400
Subject: [PATCH 1/4] Add Config model
* Add ability to configurably cache API responses per model
* Fix general error display on credentials
* Add current version from API to the documentation link
---
.../client/lib/components/modal/_index.less | 2 +-
.../lib/components/modal/modal.directive.js | 9 +-
awx/ui/client/lib/models/Base.js | 90 ++++++++++++++++---
awx/ui/client/lib/models/Config.js | 32 +++++++
awx/ui/client/lib/models/index.js | 2 +
awx/ui/client/lib/services/cache.service.js | 37 ++++++++
awx/ui/client/lib/services/index.js | 8 +-
.../smart-search/smart-search.controller.js | 13 ++-
.../smart-search/smart-search.partial.html | 2 +-
9 files changed, 171 insertions(+), 24 deletions(-)
create mode 100644 awx/ui/client/lib/models/Config.js
create mode 100644 awx/ui/client/lib/services/cache.service.js
diff --git a/awx/ui/client/lib/components/modal/_index.less b/awx/ui/client/lib/components/modal/_index.less
index ba5ac620ec..1d3b2be342 100644
--- a/awx/ui/client/lib/components/modal/_index.less
+++ b/awx/ui/client/lib/components/modal/_index.less
@@ -1,6 +1,6 @@
.at-Modal-body {
font-size: @at-font-size;
- padding: 0;
+ padding: @at-padding-panel 0;
}
.at-Modal-dismiss {
diff --git a/awx/ui/client/lib/components/modal/modal.directive.js b/awx/ui/client/lib/components/modal/modal.directive.js
index 2733bb2e1e..604f9f0495 100644
--- a/awx/ui/client/lib/components/modal/modal.directive.js
+++ b/awx/ui/client/lib/components/modal/modal.directive.js
@@ -10,13 +10,15 @@ function atModalLink (scope, el, attrs, controllers) {
});
}
-function AtModalController (eventService) {
+function AtModalController (eventService, strings) {
let vm = this;
let overlay;
let modal;
let listeners;
+ vm.strings = strings;
+
vm.init = (scope, el) => {
overlay = el[0];
modal = el.find('.at-Modal-window')[0];
@@ -67,7 +69,10 @@ function AtModalController (eventService) {
};
}
-AtModalController.$inject = ['EventService'];
+AtModalController.$inject = [
+ 'EventService',
+ 'ComponentsStrings'
+]
function atModal (pathService) {
return {
diff --git a/awx/ui/client/lib/models/Base.js b/awx/ui/client/lib/models/Base.js
index b728e93ca1..db2851bc53 100644
--- a/awx/ui/client/lib/models/Base.js
+++ b/awx/ui/client/lib/models/Base.js
@@ -1,28 +1,55 @@
let $http;
let $q;
+let cache;
function request (method, resource) {
- if (Array.isArray(method) && Array.isArray(resource)) {
- let promises = method.map((value, i) => this.http[value](resource[i]));
+ if (Array.isArray(method)) {
+ let promises = method.map((_method_, i) => {
+ return this.request(_method_, Array.isArray(resource) ? resource[i] : resource);
+ });
return $q.all(promises);
}
-
+
+ if (this.isCacheable(method, resource)) {
+ return this.requestWithCache(method, resource);
+ }
+
return this.http[method](resource);
}
+function requestWithCache (method, resource) {
+ let key = cache.createKey(method, this.path, resource);
+
+ return cache.get(key)
+ .then(data => {
+ if (data) {
+ this.model[method.toUpperCase()] = data;
+
+ return data;
+ }
+
+ return this.http[method](resource)
+ .then(res => {
+ cache.put(key, res.data);
+
+ return res;
+ });
+ });
+}
+
/**
* Intended to be useful in searching and filtering results using params
* supported by the API.
*
- * @param {object} params - An object of keys and values to to format and
+ * @arg {Object} params - An object of keys and values to to format and
* to the URL as a query string. Refer to the API documentation for the
* resource in use for specifics.
- * @param {object} config - Configuration specific to the UI to accommodate
+ * @arg {Object} config - Configuration specific to the UI to accommodate
* common use cases.
*
- * @returns {Promise} - $http
- * @yields {(Boolean|object)}
+ * @yields {boolean} - Indicating a match has been found. If so, the results
+ * are set on the model.
*/
function search (params, config) {
let req = {
@@ -279,6 +306,14 @@ function isCreatable () {
return false;
}
+function isCacheable () {
+ if (this.settings.cache === true) {
+ return true;
+ }
+
+ return false;
+}
+
function graft (id) {
let item = this.get('results').filter(result => result.id === id);
@@ -291,12 +326,27 @@ function graft (id) {
return new this.Constructor('get', item, true);
}
-function create (method, resource, graft) {
+/**
+ * `create` is called on instantiation of every model. Models can be
+ * instantiated empty or with `GET` and/or `OPTIONS` requests that yield data.
+ * If model data already exists a new instance can be created (see: `graft`)
+ * with existing data.
+ *
+ * @arg {string=} method - Populate the model with `GET` or `OPTIONS` data.
+ * @arg {(string|Object)=} resource - An `id` reference to a particular
+ * resource or an existing model's data.
+ * @arg {boolean=} graft - Create a new instance from existing model data.
+ *
+ * @returns {(Object|Promise)} - Returns a reference to the model instance
+ * if an empty instance or graft is created. Otherwise, a promise yielding
+ * a model instance is returned.
+ */
+function create (method, resource, graft, config) {
if (!method) {
return this;
}
- this.promise = this.request(method, resource);
+ this.promise = this.request(method, resource, config);
if (graft) {
return this;
@@ -306,19 +356,30 @@ function create (method, resource, graft) {
.then(() => this);
}
-function BaseModel (path) {
+/**
+ * Base functionality for API interaction.
+ *
+ * @arg {string} path - The API resource for the model extending BaseModel to
+ * use.
+ * @arg {Object=} settings - Configuration applied to all instances of the
+ * extending model.
+ * @arg {boolean=} settings.cache - Cache the model data.
+ *
+ */
+function BaseModel (path, settings) {
this.create = create;
this.find = find;
this.get = get;
this.graft = graft;
this.has = has;
this.isEditable = isEditable;
+ this.isCacheable = isCacheable;
this.isCreatable = isCreatable;
this.match = match;
- this.model = {};
this.normalizePath = normalizePath;
this.options = options;
this.request = request;
+ this.requestWithCache = requestWithCache;
this.search = search;
this.set = set;
this.unset = unset;
@@ -330,16 +391,19 @@ function BaseModel (path) {
put: httpPut.bind(this),
};
+ this.model = {};
this.path = this.normalizePath(path);
+ this.settings = settings || {};
};
-function BaseModelLoader (_$http_, _$q_) {
+function BaseModelLoader (_$http_, _$q_, _cache_) {
$http = _$http_;
$q = _$q_;
+ cache = _cache_;
return BaseModel;
}
-BaseModelLoader.$inject = ['$http', '$q'];
+BaseModelLoader.$inject = ['$http', '$q', 'CacheService'];
export default BaseModelLoader;
diff --git a/awx/ui/client/lib/models/Config.js b/awx/ui/client/lib/models/Config.js
new file mode 100644
index 0000000000..777a9f2bd4
--- /dev/null
+++ b/awx/ui/client/lib/models/Config.js
@@ -0,0 +1,32 @@
+let BaseModel;
+
+function getTruncatedVersion () {
+ let version;
+
+ try {
+ version = this.get('version').split('-')[0];
+ } catch (err) {
+ console.error(err);
+ }
+
+ return version;
+}
+
+function ConfigModel (method, resource, graft) {
+ BaseModel.call(this, 'config', { cache: true });
+
+ this.Constructor = ConfigModel;
+ this.getTruncatedVersion = getTruncatedVersion;
+
+ return this.create(method, resource, graft);
+}
+
+function ConfigModelLoader (_BaseModel_) {
+ BaseModel = _BaseModel_;
+
+ return ConfigModel;
+}
+
+ConfigModelLoader.$inject = ['BaseModel'];
+
+export default ConfigModelLoader;
diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js
index 9c48b5922b..0734340dae 100644
--- a/awx/ui/client/lib/models/index.js
+++ b/awx/ui/client/lib/models/index.js
@@ -1,4 +1,5 @@
import Base from './Base';
+import Config from './Config';
import Credential from './Credential';
import CredentialType from './CredentialType';
import Me from './Me';
@@ -7,6 +8,7 @@ import Organization from './Organization';
angular
.module('at.lib.models', [])
.service('BaseModel', Base)
+ .service('ConfigModel', Config)
.service('CredentialModel', Credential)
.service('CredentialTypeModel', CredentialType)
.service('MeModel', Me)
diff --git a/awx/ui/client/lib/services/cache.service.js b/awx/ui/client/lib/services/cache.service.js
new file mode 100644
index 0000000000..9cc526f907
--- /dev/null
+++ b/awx/ui/client/lib/services/cache.service.js
@@ -0,0 +1,37 @@
+function CacheService ($cacheFactory, $q) {
+ let cache = $cacheFactory('api');
+
+ this.put = (key, data) => {
+ return cache.put(key, data);
+ };
+
+ this.get = (key) => {
+ return $q.resolve(cache.get(key));
+ };
+
+ this.remove = (key) => {
+ if (!key) {
+ return cache.removeAll();
+ }
+
+ return cache.remove(key);
+ };
+
+ this.createKey = (method, path, resource) => {
+ let key = `${method.toUpperCase()}.${path}`;
+
+ if (resource) {
+ if (resource.id) {
+ key += `${resource.id}/`;
+ } else if (Number(resource)) {
+ key += `${resource}/`;
+ }
+ }
+
+ return key;
+ }
+}
+
+CacheService.$inject = ['$cacheFactory', '$q'];
+
+export default CacheService;
diff --git a/awx/ui/client/lib/services/index.js b/awx/ui/client/lib/services/index.js
index 85930d852d..0c7e1d0234 100644
--- a/awx/ui/client/lib/services/index.js
+++ b/awx/ui/client/lib/services/index.js
@@ -1,3 +1,4 @@
+import CacheService from './cache.service';
import EventService from './event.service';
import PathService from './path.service';
import BaseStringService from './base-string.service';
@@ -5,7 +6,8 @@ import AppStrings from './app.strings';
angular
.module('at.lib.services', [])
- .service('EventService', EventService)
- .service('PathService', PathService)
+ .service('AppStrings', AppStrings)
.service('BaseStringService', BaseStringService)
- .service('AppStrings', AppStrings);
+ .service('CacheService', CacheService)
+ .service('EventService', EventService)
+ .service('PathService', PathService);
diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js
index 69b55a45a8..7ddbfd42c7 100644
--- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js
+++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js
@@ -1,14 +1,19 @@
-export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', 'SmartSearchService', 'i18n',
- function($stateParams, $scope, $state, GetBasePath, qs, SmartSearchService, i18n) {
+export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', 'SmartSearchService', 'i18n', 'ConfigModel',
+ function($stateParams, $scope, $state, GetBasePath, qs, SmartSearchService, i18n, ConfigModel) {
let path,
defaults,
queryset,
- stateChangeSuccessListener;
+ stateChangeSuccessListener,
+ configModel = new ConfigModel();
- init();
+ configModel.request('get')
+ .then(() => init());
function init() {
+ let version = configModel.getTruncatedVersion() || 'latest';
+
+ $scope.documentationLink = `http://docs.ansible.com/ansible-tower/${version}/html/userguide/search_sort.html`;
if($scope.defaultParams) {
defaults = $scope.defaultParams;
diff --git a/awx/ui/client/src/shared/smart-search/smart-search.partial.html b/awx/ui/client/src/shared/smart-search/smart-search.partial.html
index 2dba935b0f..1f31adcf9e 100644
--- a/awx/ui/client/src/shared/smart-search/smart-search.partial.html
+++ b/awx/ui/client/src/shared/smart-search/smart-search.partial.html
@@ -50,7 +50,7 @@
From 214b72bb8dd8c30c3033a8853731cf44cc407485 Mon Sep 17 00:00:00 2001
From: gconsidine
Date: Tue, 1 Aug 2017 13:36:05 -0400
Subject: [PATCH 2/4] Use existing config service for license_type checks
---
awx/ui/client/lib/models/Config.js | 5 +++++
awx/ui/client/src/license/license.route.js | 7 +++++++
awx/ui/client/src/setup-menu/setup-menu.partial.html | 2 +-
awx/ui/client/src/setup-menu/setup.route.js | 5 ++++-
awx/ui/client/src/shared/stateExtender.provider.js | 1 +
5 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/awx/ui/client/lib/models/Config.js b/awx/ui/client/lib/models/Config.js
index 777a9f2bd4..17dd1f8901 100644
--- a/awx/ui/client/lib/models/Config.js
+++ b/awx/ui/client/lib/models/Config.js
@@ -11,12 +11,17 @@ function getTruncatedVersion () {
return version;
}
+
+function isOpen () {
+ return this.get('license_info.license_type') === 'open';
+}
function ConfigModel (method, resource, graft) {
BaseModel.call(this, 'config', { cache: true });
this.Constructor = ConfigModel;
this.getTruncatedVersion = getTruncatedVersion;
+ this.isOpen = isOpen;
return this.create(method, resource, graft);
}
diff --git a/awx/ui/client/src/license/license.route.js b/awx/ui/client/src/license/license.route.js
index cd09b28ad5..629bab4093 100644
--- a/awx/ui/client/src/license/license.route.js
+++ b/awx/ui/client/src/license/license.route.js
@@ -6,6 +6,7 @@
import {templateUrl} from '../shared/template-url/template-url.factory';
import { N_ } from '../i18n';
+import _ from 'lodash';
export default {
name: 'license',
@@ -17,6 +18,12 @@ export default {
parent: 'setup',
label: N_('LICENSE')
},
+ onEnter: ['$state', 'ConfigService', (state, configService) => {
+ return configService.getConfig()
+ .then(config => {
+ return _.get(config, 'license_info.license_type') === 'open' && state.go('setup');
+ });
+ }],
resolve: {
features: ['CheckLicense', '$rootScope',
function(CheckLicense, $rootScope) {
diff --git a/awx/ui/client/src/setup-menu/setup-menu.partial.html b/awx/ui/client/src/setup-menu/setup-menu.partial.html
index 85118a9c9e..d5179afa6f 100644
--- a/awx/ui/client/src/setup-menu/setup-menu.partial.html
+++ b/awx/ui/client/src/setup-menu/setup-menu.partial.html
@@ -68,7 +68,7 @@
View information about this version of Ansible {{BRAND_NAME}}.
-
+
View Your License
View and edit your license information.
diff --git a/awx/ui/client/src/setup-menu/setup.route.js b/awx/ui/client/src/setup-menu/setup.route.js
index 1483ac4436..51b613e32e 100644
--- a/awx/ui/client/src/setup-menu/setup.route.js
+++ b/awx/ui/client/src/setup-menu/setup.route.js
@@ -1,5 +1,6 @@
import {templateUrl} from '../shared/template-url/template-url.factory';
import { N_ } from '../i18n';
+import _ from 'lodash';
export default {
name: 'setup',
@@ -8,10 +9,12 @@ export default {
label: N_("SETTINGS")
},
templateUrl: templateUrl('setup-menu/setup-menu'),
- controller: function(orgAdmin, $scope){
+ controller: function(config, orgAdmin, $scope){
+ $scope.isOpen = _.get(config, 'license_info.license_type') === 'open';
$scope.orgAdmin = orgAdmin;
},
resolve: {
+ config: ['ConfigService', config => config.getConfig()],
orgAdmin:
['$rootScope', 'ProcessErrors', 'Rest', 'GetBasePath',
function($rootScope, ProcessErrors, Rest, GetBasePath){
diff --git a/awx/ui/client/src/shared/stateExtender.provider.js b/awx/ui/client/src/shared/stateExtender.provider.js
index f9534c2665..60edc3f083 100644
--- a/awx/ui/client/src/shared/stateExtender.provider.js
+++ b/awx/ui/client/src/shared/stateExtender.provider.js
@@ -58,6 +58,7 @@ export default function($stateProvider) {
controllerAs: state.controllerAs,
views: state.views,
parent: state.parent,
+ redirectTo: state.redirectTo,
// new in uiRouter 1.0
lazyLoad: state.lazyLoad,
};
From 384637fce8a56731a6c7d938e99832501c540a23 Mon Sep 17 00:00:00 2001
From: gconsidine
Date: Tue, 1 Aug 2017 13:40:53 -0400
Subject: [PATCH 3/4] Use ConfigService to get version for doc links
---
.../smart-search/smart-search.controller.js | 21 ++++++++++++-------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/awx/ui/client/src/shared/smart-search/smart-search.controller.js b/awx/ui/client/src/shared/smart-search/smart-search.controller.js
index 7ddbfd42c7..c889a0b98b 100644
--- a/awx/ui/client/src/shared/smart-search/smart-search.controller.js
+++ b/awx/ui/client/src/shared/smart-search/smart-search.controller.js
@@ -1,17 +1,22 @@
-export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', 'SmartSearchService', 'i18n', 'ConfigModel',
- function($stateParams, $scope, $state, GetBasePath, qs, SmartSearchService, i18n, ConfigModel) {
+export default ['$stateParams', '$scope', '$state', 'GetBasePath', 'QuerySet', 'SmartSearchService', 'i18n', 'ConfigService',
+ function($stateParams, $scope, $state, GetBasePath, qs, SmartSearchService, i18n, configService) {
let path,
defaults,
queryset,
- stateChangeSuccessListener,
- configModel = new ConfigModel();
+ stateChangeSuccessListener;
- configModel.request('get')
- .then(() => init());
+ configService.getConfig()
+ .then(config => init(config));
- function init() {
- let version = configModel.getTruncatedVersion() || 'latest';
+ function init(config) {
+ let version;
+
+ try {
+ version = config.version.split('-')[0];
+ } catch (err) {
+ version = 'latest';
+ }
$scope.documentationLink = `http://docs.ansible.com/ansible-tower/${version}/html/userguide/search_sort.html`;
From 08d3a735ab3409f491a65e69b9578800d0576eb3 Mon Sep 17 00:00:00 2001
From: gconsidine
Date: Tue, 1 Aug 2017 14:36:45 -0400
Subject: [PATCH 4/4] Add ability to submit cred form after api validation
error
---
.../credentials/edit-credentials.controller.js | 14 +++++++++-----
.../client/lib/components/form/form.directive.js | 1 -
.../lib/components/input/message.partial.html | 2 +-
3 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/awx/ui/client/features/credentials/edit-credentials.controller.js b/awx/ui/client/features/credentials/edit-credentials.controller.js
index ccdb687761..92a3dc357c 100644
--- a/awx/ui/client/features/credentials/edit-credentials.controller.js
+++ b/awx/ui/client/features/credentials/edit-credentials.controller.js
@@ -6,6 +6,9 @@ function EditCredentialsController (models, $state, $scope, strings) {
let credentialType = models.credentialType;
let organization = models.organization;
+ let omit = ['user', 'team', 'inputs'];
+ let isEditable = credential.isEditable();
+
vm.mode = 'edit';
vm.strings = strings;
vm.panelTitle = credential.get('name');
@@ -35,11 +38,12 @@ function EditCredentialsController (models, $state, $scope, strings) {
// Only exists for permissions compatibility
$scope.credential_obj = credential.get();
- vm.form = credential.createFormSchema({
- omit: ['user', 'team', 'inputs']
- });
-
- vm.form.disabled = !credential.isEditable();
+ if (isEditable) {
+ vm.form = credential.createFormSchema('put', { omit });
+ } else {
+ vm.form = credential.createFormSchema({ omit });
+ vm.form.disabled = !isEditable;
+ }
vm.form.organization._resource = 'organization';
vm.form.organization._model = organization;
diff --git a/awx/ui/client/lib/components/form/form.directive.js b/awx/ui/client/lib/components/form/form.directive.js
index e8b80898aa..e4956d1101 100644
--- a/awx/ui/client/lib/components/form/form.directive.js
+++ b/awx/ui/client/lib/components/form/form.directive.js
@@ -149,7 +149,6 @@ function AtFormController (eventService, strings) {
errorMessageSet = true;
component.state._rejected = true;
- component.state._isValid = false;
component.state._message = errors[component.state.id].join(' ');
});
}
diff --git a/awx/ui/client/lib/components/input/message.partial.html b/awx/ui/client/lib/components/input/message.partial.html
index 00951434a6..cb73ab9620 100644
--- a/awx/ui/client/lib/components/input/message.partial.html
+++ b/awx/ui/client/lib/components/input/message.partial.html
@@ -1,4 +1,4 @@
-
+
{{ state._message }}