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_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']
|
||||||
|
@ -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': {
|
||||||
|
@ -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')
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
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')
|
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
|
||||||
};
|
};
|
||||||
}])
|
}])
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user