Implement creating new App from other App Creation dialog.

Any field type unspecified in dynamic UI code is considered to be
fully qualified name (FQN) of some Application. Currently only apps of
that exact FQN will be present in drop-down list, in future all
descendants will also be supported.

New form field/widget (as well as js-code) is taken almost ready from
horizon.forms.fields.DynamicChoiceFields, with a bit of copy-paste
from horizon in django Wizard done() method (it is not possible to
directly inherit the required functionality from
horizon.forms.views.ModalFormView, because it is incompatible with
django WizardView.

Change-Id: I942884a9a98857c13002095c8d8bb96331d2e555
Implements: blueprint new-web-ui-prototype
This commit is contained in:
Timur Sufiev 2014-03-26 15:26:15 +04:00
parent ef985ae7a1
commit 21604e9329
8 changed files with 117 additions and 20 deletions

View File

@ -79,6 +79,15 @@ class AppCatalogObjects(object):
def get(self, **kwargs):
return self.app_list[kwargs.pop('app_id')]
def filter(self, **kwargs):
def filter_func(_app):
for key, value in kwargs.iteritems():
if not hasattr(_app, key) or getattr(_app, key) != value:
return False
return True
return [app for app in self.app_list.itervalues() if filter_func(app)]
def get_ui(self, **kwargs):
app_id = kwargs.pop('app_id')
url = '{0}/{1}/ui'.format(self.url, app_id)

View File

@ -28,6 +28,7 @@ import copy
import types
import logging
import horizon.tables as tables
import horizon.forms as hz_forms
import floppyforms
from django.template.loader import render_to_string
from muranoclient.common import exceptions as muranoclient_exc
@ -657,3 +658,23 @@ class PostgreSqlChoiceField(ChoiceField):
self.choices = [('', _('(No Database)'))]
psql = api.service_list_by_type(request, environment_id, 'postgreSql')
self.choices.extend([(srv.id, srv.name) for srv in psql])
def make_select_cls(fqn):
class DynamicSelect(hz_forms.DynamicChoiceField, CustomPropertiesField):
def __init__(self, *args, **kwargs):
super(DynamicSelect, self).__init__(*args, **kwargs)
self.widget.add_item_link = 'horizon:murano:catalog:add'
@with_request
def update(self, request, environment_id, **kwargs):
app_id = api.app_id_by_fqn(request, fqn)
if app_id is None:
msg = "Application with FQN='{0}' doesn't exist"
raise KeyError(msg.format(fqn))
self.widget.add_item_link_args = (environment_id, app_id)
self.choices = [('', _('Select Application'))]
apps = api.service_list_by_id(request, environment_id, app_id)
self.choices.extend([(app.id, app.name) for app in apps])
return DynamicSelect

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
import logging
import types
@ -24,7 +25,14 @@ import yaql
log = logging.getLogger(__name__)
TYPES = {
class AnyFieldDict(collections.defaultdict):
def __missing__(self, key):
return fields.make_select_cls(key)
TYPES = AnyFieldDict()
TYPES.update({
'string': fields.CharField,
'boolean': fields.BooleanField,
'clusterip': fields.ClusterIPField,
@ -40,7 +48,7 @@ TYPES = {
'text': (fields.CharField, forms.Textarea),
'floatingip': fields.FloatingIpBooleanField,
'psqlDatabase': fields.PostgreSqlChoiceField
}
})
def _collect_fields(field_specs, form_name, service):

View File

@ -25,6 +25,7 @@ from consts import STATUS_ID_READY, STATUS_ID_NEW
from .network import get_network_params
from muranodashboard.environments import format
from muranodashboard.catalog import models
log = logging.getLogger(__name__)
@ -258,11 +259,15 @@ def services_list(request, environment_id):
return [bunch.bunchify(service) for service in services]
def service_list_by_type(request, environment_id, service_type):
def app_id_by_fqn(request, fqn):
apps = models.AppCatalogModel().objects.filter(fqn=fqn)
return apps[0].id if apps else None
def service_list_by_id(request, environment_id, app_id):
services = services_list(request, environment_id)
log.debug('Service::Instances::List')
return [service for service in services
if service['type'] == service_type]
return [service for service in services if service['app_id'] == app_id]
def service_create(request, environment_id, parameters):

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import logging
import copy
from functools import update_wrapper
@ -27,7 +28,7 @@ from horizon import tabs
from horizon import tables
from horizon import workflows
from horizon import messages
from horizon.forms.views import ModalFormMixin
from horizon.forms import views as hz_views
from tables import EnvironmentsTable
from tables import DeploymentsTable
from tables import EnvConfigTable
@ -41,6 +42,7 @@ from muranodashboard.dynamic_ui.services import get_service_name
from muranodashboard.dynamic_ui.services import get_service_field_descriptions
from muranodashboard.catalog import models
from muranodashboard.dynamic_ui import helpers
from muranodashboard import utils
LOG = logging.getLogger(__name__)
@ -89,9 +91,14 @@ class LazyWizard(SessionWizardView):
return view
class Wizard(ModalFormMixin, LazyWizard):
class Wizard(hz_views.ModalFormMixin, LazyWizard):
template_name = 'services/wizard_create.html'
def get_prefix(self, *args, **kwargs):
base = super(Wizard, self).get_prefix(*args, **kwargs)
fmt = utils.BlankFormatter()
return fmt.format('{0}_{environment_id}_{app_id}', base, **kwargs)
def done(self, form_list, **kwargs):
environment_id = kwargs.get('environment_id')
url = reverse('horizon:murano:environments:services',
@ -105,22 +112,35 @@ class Wizard(ModalFormMixin, LazyWizard):
attributes = helpers.insert_hidden_ids(attributes)
try:
api.service_create(self.request, environment_id, attributes)
srv = api.service_create(self.request, environment_id, attributes)
except HTTPForbidden:
msg = _('Sorry, you can\'t create service right now.'
'The environment is deploying.')
msg = _("Sorry, you can\'t add application right now."
"The environment is deploying.")
redirect = reverse("horizon:murano:environments:index")
exceptions.handle(self.request, msg, redirect=redirect)
except Exception:
redirect = reverse("horizon:murano:environments:index")
exceptions.handle(self.request,
_('Sorry, you can\'t create service right now.'),
_("Sorry, you can't add application right now."),
redirect=redirect)
else:
message = "The %s service successfully created." % \
get_service_name(self.request, app_id)
messages.success(self.request, message)
return HttpResponseRedirect(url)
return self.create_hacked_response(srv.id, attributes['name'])
#return HttpResponseRedirect(url)
def create_hacked_response(self, obj_id, obj_name):
# copy-paste from horizon.forms.views.ModalFormView; should be done
# that way until we move here from django Wizard to horizon workflow
if hz_views.ADD_TO_FIELD_HEADER in self.request.META:
field_id = self.request.META[hz_views.ADD_TO_FIELD_HEADER]
response = HttpResponse(json.dumps([obj_id, obj_name]))
response["X-Horizon-Add-To-Field"] = field_id
return response
else:
return HttpResponse('Done')
def get_form_initial(self, step):
init_dict = {}
@ -140,7 +160,8 @@ class Wizard(ModalFormMixin, LazyWizard):
context.update({'type': app.fqn,
'service_name': app.name,
'app_id': app_id,
'environment_id': self.kwargs.get('environment_id')
'environment_id': self.kwargs.get('environment_id'),
'prefix': self.prefix
})
return context

View File

@ -41,14 +41,18 @@
{% block modal-footer %}
<script type="text/javascript">
$(function() {
$(".btn-wizard").click(function() {
$('#wizard_goto_step').val('{{ wizard.steps.prev }}')
{# Make element ids unique per-wizard to avoid interference #}
{# upon pressing 'Back' button while creating one application #}
{# from another #}
var btn_id = '#{{ prefix }}_btn', val_id = '#{{ prefix }}_val';
$(btn_id).click(function() {
$(val_id).val('{{ wizard.steps.prev }}')
});
});
</script>
{{ wizard.form.media }}
<input type="hidden" name="wizard_goto_step" id="wizard_goto_step"/>
<input type="hidden" name="wizard_goto_step" id="{{ prefix }}_val"/>
{% if wizard.steps.next %}
{% trans "Next" as next %}
{% else %}
@ -56,10 +60,10 @@
{% endif %}
{% if wizard.steps.index > 0 %}
<input type="submit" class="btn btn-primary pull-right" value="{{ next }}"/>
<button name="wizard_goto_step" type="submit" class="btn btn-small btn-wizard">
{% trans "Back" %}</button>
<button name="wizard_goto_step" type="submit" class="btn btn-small"
id="{{ prefix }}_btn">{% trans "Back" %}</button>
{% else %}
<button name="wizard_goto_step" type="submit" class="btn btn-small btn-wizard">
<button name="wizard_goto_step" type="submit" class="btn btn-small">
{{ next }}</button>
{% endif %}
{% endblock %}

29
muranodashboard/utils.py Normal file
View File

@ -0,0 +1,29 @@
# Copyright (c) 2014 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import string
class BlankFormatter(string.Formatter):
"""
Utility class aimed to provide empty string for non-existent keys.
"""
def __init__(self, default=''):
self.default = default
def get_value(self, key, args, kwargs):
if isinstance(key, str):
return kwargs.get(key, self.default)
else:
return string.Formatter.get_value(self, key, args, kwargs)

View File

@ -6,5 +6,5 @@ six>=1.5.2
PyYAML>=3.1.0
django-floppyforms>=1.1
yaql==0.2
python-muranoclient==0.4.1
http://tarballs.openstack.org/python-muranoclient/python-muranoclient-master.tar.gz#egg=muranoclient-0.5
murano-metadataclient==0.4.1