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:
parent
85ac430e3f
commit
05f4f22b9e
@ -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']
|
||||
|
@ -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': {
|
||||
|
@ -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')
|
||||
)
|
||||
|
@ -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
|
||||
|
4212
merlin/static/merlin/js/lib/ui-bootstrap-tpls-0.12.0.js
Normal file
4212
merlin/static/merlin/js/lib/ui-bootstrap-tpls-0.12.0.js
Normal file
File diff suppressed because it is too large
Load Diff
4209
merlin/static/merlin/js/lib/ui-bootstrap-tpls-0.12.1.js
Normal file
4209
merlin/static/merlin/js/lib/ui-bootstrap-tpls-0.12.1.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
};
|
||||
}])
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user