Implement auto-completion field in Merlin

Also provide first approach to changing dependent field on field
change according to schema received by $http AJAX call.

This commit includes updated angular-bootstrap code is well for the
typeahead plugin to work with ng-model-options="{getterSetter: true}"
option. The ideal solution here would be to create a pull-request to the
angular-bootstrap plugin repo at github, make it merged and then make
the new version to be used in Horizon (and eliminate the need to use the
customized version of angular-bootstrap plugin in Merlin).

Implements: blueprint angular-fields-dependencies
Change-Id: I2be49de07beb09f430a8a4ffe5a19552fbaeb81e
This commit is contained in:
Timur Sufiev 2015-03-11 18:25:10 +03:00
parent 85ac430e3f
commit 05f4f22b9e
8 changed files with 8545 additions and 11 deletions

View File

@ -12,6 +12,7 @@ ADD_PANEL = 'mistral.panel.MistralPanel'
ADD_ANGULAR_MODULES = ['angular.filter', 'merlin', 'mistral'] ADD_ANGULAR_MODULES = ['angular.filter', 'merlin', 'mistral']
ADD_JS_FILES = ['merlin/js/lib/angular-filter.js', ADD_JS_FILES = ['merlin/js/lib/angular-filter.js',
'merlin/js/lib/ui-bootstrap-tpls-0.12.1.js',
'merlin/js/merlin.init.js', 'merlin/js/merlin.init.js',
'merlin/js/merlin.templates.js', 'merlin/js/merlin.templates.js',
'mistral/js/mistral.init.js'] 'mistral/js/mistral.init.js']

View File

@ -6,8 +6,8 @@
angular.module('mistral') angular.module('mistral')
.factory('mistral.workbook.models', .factory('mistral.workbook.models',
['merlin.field.models', 'merlin.panel.models', 'merlin.utils', ['merlin.field.models', 'merlin.panel.models', 'merlin.utils', '$http', '$q',
function(fields, panel, utils) { function(fields, panel, utils, $http, $q) {
var models = {}; var models = {};
function varlistValueFactory(json, parameters) { function varlistValueFactory(json, parameters) {
@ -104,6 +104,22 @@
}); });
models.Action = fields.frozendict.extend({ models.Action = fields.frozendict.extend({
create: function(json, parameters) {
var self = fields.frozendict.create.call(this, json, parameters),
base = self.get('base');
base.on('change', function(operation) {
var baseValue;
if ( operation != 'id' ) {
baseValue = base.get();
if ( baseValue ) {
base.getSchema(baseValue).then(function(keys) {
self.get('base-input').setSchema(keys);
});
}
}
});
return self;
},
_getPrettyJSON: function() { _getPrettyJSON: function() {
var json = fields.frozendict._getPrettyJSON.apply(this, arguments); var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
delete json.name; delete json.name;
@ -119,21 +135,48 @@
}) })
}, },
'base': { 'base': {
'@class': fields.string.extend({}, { '@class': fields.string.extend({
create: function(json, parameters) {
var self = fields.string.create.call(this, json, parameters),
schema = {},
url = utils.getMeta(self, 'autocompletionUrl');
self.getSchema = function(key) {
var deferred = $q.defer();
if ( !(key in schema) ) {
$http.get(url+'?key='+key).success(function(keys) {
schema[key] = keys;
deferred.resolve(keys);
}).error(function() {
deferred.reject();
});
} else {
deferred.resolve(schema[key]);
}
return deferred.promise;
};
return self;
}
}, {
'@meta': { '@meta': {
'index': 1, 'index': 1,
'row': 0 'row': 0,
autocompletionUrl: '/project/mistral/actions/types'
} }
}) })
}, },
'base-input': { 'base-input': {
'@class': fields.frozendict.extend({}, { '@class': fields.directeddictionary.extend({}, {
'@required': false, '@required': false,
'@meta': { '@meta': {
'index': 2, 'index': 2,
'title': 'Base Input' 'title': 'Base Input'
}, },
'?': {'@class': fields.string} '?': {
'@class': fields.string.extend({}, {
'@meta': {'row': 1}
})
}
}) })
}, },
'input': { 'input': {

View File

@ -20,4 +20,5 @@ from mistral import views
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^$', views.IndexView.as_view(), name='index'), url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create$', views.CreateWorkbookView.as_view(), name='create'), url(r'^create$', views.CreateWorkbookView.as_view(), name='create'),
url(r'^actions/types$', views.ActionTypesView.as_view(), name='action_types')
) )

View File

@ -12,8 +12,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django import views from django import http
from django.views.generic import View
from horizon import tables from horizon import tables
from horizon.views import APIView from horizon.views import APIView
import yaml import yaml
@ -27,6 +30,25 @@ class CreateWorkbookView(APIView):
template_name = 'project/mistral/create.html' template_name = 'project/mistral/create.html'
class ActionTypesView(View):
def get(self, request, *args, **kwargs):
key = request.GET.get('key')
schema = {
'nova.create_server': ['image', 'flavor', 'network_id'],
'neutron.create_network': ['name', 'create_subnet'],
'glance.create_image': ['image_url']
}
response = http.HttpResponse(content_type='application/json')
if key:
result = schema.get(key)
if result is None:
return http.HttpResponse(status=404)
response.write(json.dumps(schema.get(key)))
else:
response.write(json.dumps(schema.keys()))
return response
class IndexView(tables.DataTableView): class IndexView(tables.DataTableView):
template_name = 'project/mistral/index.html' template_name = 'project/mistral/index.html'
table_class = mistral_tables.WorkbooksTable table_class = mistral_tables.WorkbooksTable

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
angular.module('merlin') angular.module('merlin')
.factory('merlin.field.models', .factory('merlin.field.models',
['merlin.utils', 'merlin.panel.models', function(utils, panels) { ['merlin.utils', 'merlin.panel.models', '$http', function(utils, panels, $http) {
var wildcardMixin = Barricade.Blueprint.create(function() { var wildcardMixin = Barricade.Blueprint.create(function() {
return this; return this;
@ -68,6 +68,10 @@
if ( this.getEnumValues ) { if ( this.getEnumValues ) {
restrictedChoicesMixin.call(this); restrictedChoicesMixin.call(this);
} }
var autocompletionUrl = utils.getMeta(this, 'autocompletionUrl');
if ( autocompletionUrl ) {
autoCompletionMixin.call(this, autocompletionUrl);
}
return this; return this;
}); });
@ -84,6 +88,20 @@
} }
}, {'@type': String}); }, {'@type': String});
var autoCompletionMixin = Barricade.Blueprint.create(function(url) {
var suggestions = [];
$http.get(url).success(function(data) {
suggestions = data;
});
this.getSuggestions = function() {
return suggestions;
};
return this;
});
var textModel = Barricade.Primitive.extend({ var textModel = Barricade.Primitive.extend({
create: function(json, parameters) { create: function(json, parameters) {
var self = Barricade.Primitive.create.call(this, json, parameters); var self = Barricade.Primitive.create.call(this, json, parameters);
@ -149,10 +167,10 @@
modelMixin.call(self, 'dictionary'); modelMixin.call(self, 'dictionary');
self.add = function() { self.add = function(newID) {
var regexp = new RegExp('(' + baseKey + ')([0-9]+)'), var regexp = new RegExp('(' + baseKey + ')([0-9]+)'),
newID = baseKey + utils.getNextIDSuffix(self, regexp),
newValue; newValue;
newID = newID || baseKey + utils.getNextIDSuffix(self, regexp);
if ( _elClass.instanceof(Barricade.ImmutableObject) ) { if ( _elClass.instanceof(Barricade.ImmutableObject) ) {
if ( 'name' in _elClass._schema ) { if ( 'name' in _elClass._schema ) {
var nameNum = utils.getNextIDSuffix(self, regexp); var nameNum = utils.getNextIDSuffix(self, regexp);
@ -186,6 +204,27 @@
} }
}, {'@type': Object}); }, {'@type': Object});
var directedDictionaryModel = dictionaryModel.extend({
create: function(json, parameters) {
var self = dictionaryModel.create.call(this, json, parameters);
self.setType('frozendict');
return self;
},
setSchema: function(keys) {
var self = this;
if ( keys !== undefined && keys !== null ) {
self.getIDs().forEach(function(oldKey) {
self.remove(oldKey);
});
keys.forEach(function(newKey) {
self.add(newKey);
});
}
}
}, {
'?': {'@type': String}
});
return { return {
string: stringModel, string: stringModel,
text: textModel, text: textModel,
@ -193,6 +232,7 @@
list: listModel, list: listModel,
dictionary: dictionaryModel, dictionary: dictionaryModel,
frozendict: frozendictModel, frozendict: frozendictModel,
directeddictionary: directedDictionaryModel,
wildcard: wildcardMixin // use for most general type-checks wildcard: wildcardMixin // use for most general type-checks
}; };
}]) }])

View File

@ -1,5 +1,11 @@
<div class="form-group"> <div class="form-group">
<label for="elem-{$ $id $}">{$ title $}</label> <label for="elem-{$ $id $}">{$ title $}</label>
<input type="text" class="form-control" id="elem-{$ $id $}" ng-model="value.value" <input ng-if="!value.getSuggestions"
type="text" class="form-control" id="elem-{$ $id $}" ng-model="value.value"
ng-model-options="{ getterSetter: true }"> ng-model-options="{ getterSetter: true }">
<input ng-if="value.getSuggestions"
type="text" class="form-control" id="elem-{$ $id $}" ng-model="value.value"
ng-model-options="{ getterSetter: true }"
typeahead="option for option in value.getSuggestions() | filter:$viewValue"
typeahead-editable="true">
</div> </div>