Add flavor saving

Properly save the flavor of the role.
Also bring back the autosave.
Also add menu for non-drag-and-drop adding.

Change-Id: Ie977aa2c1eb23b26589a881b65db6840046b5c59
This commit is contained in:
Radomir Dopieralski 2014-10-03 16:41:23 +02:00
parent 05e505c5c6
commit 7d8210b65a
4 changed files with 134 additions and 16 deletions

View File

@ -2,3 +2,6 @@ PANEL = 'overview'
PANEL_DASHBOARD = 'infrastructure' PANEL_DASHBOARD = 'infrastructure'
PANEL_GROUP = 'infrastructure' PANEL_GROUP = 'infrastructure'
ADD_PANEL = 'tuskar_boxes.overview.panel.Overview' ADD_PANEL = 'tuskar_boxes.overview.panel.Overview'
ADD_INSTALLED_APPS = [
'tuskar_boxes',
]

View File

@ -0,0 +1,59 @@
# -*- coding: utf8 -*-
#
# 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 logging
import django.forms
from django.utils.translation import ugettext_lazy as _
import horizon.exceptions
from tuskar_ui.infrastructure.overview import forms
LOG = logging.getLogger(__name__)
class EditPlan(forms.EditPlan):
def __init__(self, *args, **kwargs):
super(EditPlan, self).__init__(*args, **kwargs)
self.fields.update(self._role_flavor_fields(self.plan))
def _role_flavor_fields(self, plan):
fields = {}
for role in plan.role_list:
field = django.forms.CharField(
label=_("Flavor for {0}").format(role.name),
initial=role.flavor(plan).name if role.flavor(plan) else '',
required=False,
widget=django.forms.HiddenInput(attrs={
'class': "boxes-flavor",
}),
)
field.role = role
fields['%s-flavor' % role.id] = field
return fields
def handle(self, request, data):
result = super(EditPlan, self).handle(request, data)
parameters = dict(
(field.role.flavor_parameter_name, data[name])
for (name, field) in self.fields.items() if name.endswith('-flavor')
)
try:
self.plan = self.plan.patch(request, self.plan.uuid, parameters)
except Exception as e:
horizon.exceptions.handle(request, _("Unable to update the plan."))
LOG.exception(e)
return False
return result

View File

@ -14,6 +14,7 @@
from tuskar_ui import api from tuskar_ui import api
from tuskar_ui.infrastructure.overview import views from tuskar_ui.infrastructure.overview import views
from tuskar_boxes.overview import forms
def flavor_nodes(request, flavor): def flavor_nodes(request, flavor):
@ -27,25 +28,38 @@ def flavor_nodes(request, flavor):
]): ]):
yield node yield node
class IndexView(views.IndexView): class IndexView(views.IndexView):
template_name = "tuskar_boxes/overview/index.html" template_name = "tuskar_boxes/overview/index.html"
form_class = forms.EditPlan
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
context = super(IndexView, self).get_context_data(*args, **kwargs) context = super(IndexView, self).get_context_data(*args, **kwargs)
flavors = api.flavor.Flavor.list(self.request) flavors = api.flavor.Flavor.list(self.request)
flavors.sort(key=lambda np: (np.vcpus, np.ram, np.disk)) flavors.sort(key=lambda np: (np.vcpus, np.ram, np.disk))
for role in context['roles']:
flavor = role['role'].flavor(context['plan'])
role['flavor_name'] = flavor.name if flavor else ''
context['flavors'] = [] context['flavors'] = []
for flavor in flavors: for flavor in flavors:
nodes = [{ nodes = [{
'role': '', 'role': '',
} for node in flavor_nodes(self.request, flavor)] } for node in flavor_nodes(self.request, flavor)]
roles = [role for role in context['roles']
if role['flavor_name'] == flavor.name]
flavor = { flavor = {
'name': flavor.name, 'name': flavor.name,
'vcpus': flavor.vcpus, 'vcpus': flavor.vcpus,
'ram': flavor.ram, 'ram': flavor.ram,
'disk': flavor.disk, 'disk': flavor.disk,
'nodes': nodes, 'nodes': nodes,
'roles': roles,
} }
if nodes: # Don't list empty flavors if nodes or roles: # Don't list empty flavors
context['flavors'].append(flavor) context['flavors'].append(flavor)
context['free_roles'] = [role for role in context['roles']
if not role['flavor_name']]
if not context['stack']:
for role in context['roles']:
role['flavor_field'] = context['form'][role['id'] + '-flavor']
return context return context

View File

@ -3,14 +3,14 @@
{% load form_helpers %} {% load form_helpers %}
<h4>{% trans "Available Deployment Roles" %}</h4> <h4>{% trans "Available Deployment Roles" %}</h4>
<form method="POST" action="." class="boxes-form"> <form method="POST" action="." class="deployment-roles-form">
{% csrf_token %} {% csrf_token %}
{% include 'horizon/common/_form_errors.html' with form=form %}
<div class="boxes-available-roles"> <div class="boxes-available-roles">
{% for role in roles %}{% spaceless %} {% for role in free_roles %}{% spaceless %}
<div class="boxes-role-{{ role.name|slugify }} boxes-role" data-name="{{ role.name|slugify }}"> <div class="boxes-role-{{ role.name|slugify }} boxes-role" data-name="{{ role.name|slugify }}">
{{ role.field|add_bootstrap_class }} {{ role.field|add_bootstrap_class }}
{{ role.flavor_field|add_bootstrap_class }}
{{ role.name|title }} {{ role.name|title }}
</div> </div>
{% endspaceless %}{% endfor %} {% endspaceless %}{% endfor %}
@ -18,7 +18,7 @@
<h4>{% trans "Available Node Profiles" %}</h4> <h4>{% trans "Available Node Profiles" %}</h4>
{% for flavor in flavors %} {% for flavor in flavors %}
<div class="boxes-flavor"> <div class="boxes-flavor" data-flavor="{{ flavor.name }}">
<p> <p>
<strong>Node Profile:</strong> <strong>Node Profile:</strong>
<i>{{ flavor.name }}</i> <i>{{ flavor.name }}</i>
@ -29,9 +29,24 @@
<div class="row"> <div class="row">
<div class="col-xs-5"> <div class="col-xs-5">
<div class="boxes-drop-roles"> <div class="boxes-drop-roles">
{% for role in flavor.roles %}{% spaceless %}
<div class="boxes-role-{{ role.name|slugify }} boxes-role" data-name="{{ role.name|slugify }}">
{{ role.field|add_bootstrap_class }}
{{ role.flavor_field|add_bootstrap_class }}
{{ role.name|title }}
</div>
{% endspaceless %}{% endfor %}
</div> </div>
<div class="boxes-drop"> <div class="boxes-drop">
<i class="fa fa-plus"></i> <div><i class="fa fa-plus"></i><br>
{% trans "enroll a deployment role" %}</div>
<div class="boxes-roles-menu btn-group-vertical">
{% for role in roles %}
<div class="btn" data-role="{{ role.name }}">
{{ role.name|title }}
</div>
{% endfor %}
</div>
</div> </div>
</div> </div>
<div class="col-xs-7 boxes-nodes"> <div class="col-xs-7 boxes-nodes">
@ -91,7 +106,7 @@
$('div.boxes-role').draggable({ $('div.boxes-role').draggable({
revert: 'invalid', revert: 'invalid',
helper: 'clone', helper: 'clone',
stack: '.boxes-roles', zIndex: 1000,
opacity: 0.5 opacity: 0.5
}); });
$('div.boxes-drop').droppable({ $('div.boxes-drop').droppable({
@ -101,7 +116,11 @@
tolerance: 'touch', tolerance: 'touch',
drop: function (ev, ui) { drop: function (ev, ui) {
ui.draggable.appendTo($(this).prev('.boxes-drop-roles')); ui.draggable.appendTo($(this).prev('.boxes-drop-roles'));
ui.draggable.find('input.number-picker').val(1); var $count = ui.draggable.find('input.number-picker');
if (+$count.val() < 1) { $count.val(1); }
ui.draggable.find('input.boxes-flavor'
).val($(this).closest('.boxes-flavor').data('flavor'));
$count.trigger('change');
window.setTimeout(update_boxes, 0); window.setTimeout(update_boxes, 0);
} }
}); });
@ -112,17 +131,41 @@
tolerance: 'touch', tolerance: 'touch',
drop: function (ev, ui) { drop: function (ev, ui) {
ui.draggable.appendTo(this); ui.draggable.appendTo(this);
ui.draggable.find('input.number-picker').val(0); ui.draggable.find('input.boxes-flavor').val('');
ui.draggable.find('input.number-picker').trigger('change').val(0);
window.setTimeout(update_boxes, 0); window.setTimeout(update_boxes, 0);
} }
}); });
update_boxes(); update_boxes();
$('input.number-picker').change(update_boxes); $('input.number-picker').change(update_boxes);
$('.boxes-roles-menu').hide().parent().click(function () {
$(this).find('.boxes-roles-menu').toggle('fold', 100);
});
$('.boxes-roles-menu .btn').click(function () {
var name = $(this).data('role');
var $drop = $(this).closest('.boxes-drop').prev('.boxes-drop-roles');
var $role = $('.boxes-role[data-name="' + name + '"]');
var $count = $role.find('input.number-picker');
var $flavor = $role.find('input.boxes-flavor');
$role.appendTo($drop);
if (+$count.val() < 1) { $count.val(1); }
$flavor.val($drop.closest('.boxes-flavor').data('flavor'));
$count.trigger('change');
window.setTimeout(update_boxes, 0);
});
}); });
</script> </script>
<style type="text/css"> <style type="text/css">
.boxes-roles-menu {
position: absolute;
z-index: 1000;
width: 100%;
left: 0;
top: 48px;
}
.boxes-node { .boxes-node {
display: inline-block; display: inline-block;
width: 60px; width: 60px;
@ -136,7 +179,7 @@
padding: 20px 4px 0 4px; padding: 20px 4px 0 4px;
} }
.boxes-available-roles { .boxes-available-roles {
border-radius: 2px; border-radius: 4px;
background: #eee; background: #eee;
border: 1px dashed #666; border: 1px dashed #666;
min-height: 42px; min-height: 42px;
@ -145,20 +188,20 @@
padding: 4px 0 0 4px; padding: 4px 0 0 4px;
} }
.boxes-role { .boxes-role {
display: inline-block;
padding: 6px; padding: 6px;
border: 1px solid; border: 1px solid;
width: 180px;
cursor: move; cursor: move;
border-radius: 2px; border-radius: 2px;
margin: 0 4px 4px 0;
background-color: #fce94f; background-color: #fce94f;
border-color: #edd400; border-color: #edd400;
color: #c4a000; color: #c4a000;
margin: 0 0 4px 0;
} }
.boxes-available-roles .boxes-role { .boxes-available-roles .boxes-role {
display: inline-block;
text-align: center; text-align: center;
width: 120px; width: 120px;
margin: 0 4px 4px 0;
} }
.boxes-available-roles .boxes-role .form-control { .boxes-available-roles .boxes-role .form-control {
display: none; display: none;
@ -184,12 +227,11 @@
color: #5c3566; color: #5c3566;
} }
.boxes-drop { .boxes-drop {
display: inline-block; position: relative;
padding: 6px; padding: 6px;
border: 1px dashed; border: 1px dashed;
width: 180px;
text-align: center; text-align: center;
border-radius: 2px; border-radius: 4px;
background-color: #eee; background-color: #eee;
border-color: #666; border-color: #666;
color: #444; color: #444;