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_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.templates.js',
'mistral/js/mistral.init.js']

View File

@ -6,8 +6,8 @@
angular.module('mistral')
.factory('mistral.workbook.models',
['merlin.field.models', 'merlin.panel.models', 'merlin.utils',
function(fields, panel, utils) {
['merlin.field.models', 'merlin.panel.models', 'merlin.utils', '$http', '$q',
function(fields, panel, utils, $http, $q) {
var models = {};
function varlistValueFactory(json, parameters) {
@ -104,6 +104,22 @@
});
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() {
var json = fields.frozendict._getPrettyJSON.apply(this, arguments);
delete json.name;
@ -119,21 +135,48 @@
})
},
'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': {
'index': 1,
'row': 0
'row': 0,
autocompletionUrl: '/project/mistral/actions/types'
}
})
},
'base-input': {
'@class': fields.frozendict.extend({}, {
'@class': fields.directeddictionary.extend({}, {
'@required': false,
'@meta': {
'index': 2,
'title': 'Base Input'
},
'?': {'@class': fields.string}
'?': {
'@class': fields.string.extend({}, {
'@meta': {'row': 1}
})
}
})
},
'input': {

View File

@ -20,4 +20,5 @@ from mistral import views
urlpatterns = patterns('',
url(r'^$', views.IndexView.as_view(), name='index'),
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
# under the License.
import json
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.views import APIView
import yaml
@ -27,6 +30,25 @@ class CreateWorkbookView(APIView):
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):
template_name = 'project/mistral/index.html'
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')
.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() {
return this;
@ -68,6 +68,10 @@
if ( this.getEnumValues ) {
restrictedChoicesMixin.call(this);
}
var autocompletionUrl = utils.getMeta(this, 'autocompletionUrl');
if ( autocompletionUrl ) {
autoCompletionMixin.call(this, autocompletionUrl);
}
return this;
});
@ -84,6 +88,20 @@
}
}, {'@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({
create: function(json, parameters) {
var self = Barricade.Primitive.create.call(this, json, parameters);
@ -149,10 +167,10 @@
modelMixin.call(self, 'dictionary');
self.add = function() {
self.add = function(newID) {
var regexp = new RegExp('(' + baseKey + ')([0-9]+)'),
newID = baseKey + utils.getNextIDSuffix(self, regexp),
newValue;
newID = newID || baseKey + utils.getNextIDSuffix(self, regexp);
if ( _elClass.instanceof(Barricade.ImmutableObject) ) {
if ( 'name' in _elClass._schema ) {
var nameNum = utils.getNextIDSuffix(self, regexp);
@ -186,6 +204,27 @@
}
}, {'@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 {
string: stringModel,
text: textModel,
@ -193,6 +232,7 @@
list: listModel,
dictionary: dictionaryModel,
frozendict: frozendictModel,
directeddictionary: directedDictionaryModel,
wildcard: wildcardMixin // use for most general type-checks
};
}])

View File

@ -1,5 +1,11 @@
<div class="form-group">
<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 }">
<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>