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:
parent
05e505c5c6
commit
7d8210b65a
@ -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',
|
||||||
|
]
|
||||||
|
59
tuskar_boxes/overview/forms.py
Normal file
59
tuskar_boxes/overview/forms.py
Normal 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
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user