Sexy boxes drag and drop

The plan edit page is modified to show the flavors with a visualisation
of the nodes. Roles can be dragged and dropped on the flavors.

TODO: actually save the flavor for a role in the form
TODO: add a popup for adding the roles to flavors

Change-Id: I9b97f39e3800b5b6c2e5639fcfd186b4196d0190
This commit is contained in:
Radomir Dopieralski 2014-10-02 15:43:44 +02:00
parent f1bf2337ae
commit 05e505c5c6
21 changed files with 249 additions and 411 deletions

View File

@ -7,7 +7,7 @@ include AUTHORS
include LICENSE
include Makefile
include manage.py
include README.md
include README.rst
include run_tests.sh
include tox.ini
include doc/Makefile

View File

@ -16,7 +16,8 @@ from django.utils.translation import ugettext_lazy as _
import horizon
from tuskar_ui.infrastructure import dashboard
import tuskar_ui.infrastructure.dashboard as tuskar_dashboard
from tuskar_ui.infrastructure.overview.panel import Overview as TuskarOverview
class Overview(horizon.Panel):
@ -24,4 +25,5 @@ class Overview(horizon.Panel):
slug = "overview"
dashboard.Infrastructure.register(Overview)
tuskar_dashboard.Infrastructure.unregister(TuskarOverview)
tuskar_dashboard.Infrastructure.register(Overview)

View File

@ -1,26 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}provision_form{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:overview:deploy_confirmation' %}{% endblock %}
{% block modal_id %}provision_modal{% endblock %}
{% block modal-header %}{% trans "Deployment Confirmation" %}{% endblock %}
{% block modal-body %}
<div>
<p>{% trans "You are about deploy your overcloud" %}
</p>
{% if autogenerated_parameters %}
<strong>These parameters will be randomly generated before the deployment:</strong>
<p>{{ autogenerated_parameters|join:", " }}</p>
{% endif %}
<p>{% trans "This operation cannot be undone. Are you sure you want to do that?" %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary" type="submit" value="{% trans "Deploy" %}" />
<a href="{% url 'horizon:infrastructure:overview:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -1,27 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}post_deploy_init_form{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:overview:post_deploy_init' %}{% endblock %}
{% block modal_id %}provision_modal{% endblock %}
{% block modal-header %}{% trans "Initialize Overcloud" %}{% endblock %}
{% block modal-body %}
<div>
<fieldset>
<div class="left">
{% include "horizon/common/_form_fields.html" %}
</div>
<div class="right">
{% trans "Your OpenStack cloud nodes are deployed. They need to be initialized before your cloud will be live."%}
</div>
</fieldset>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary" type="submit" value="{% trans "Initialize" %}" />
<a href="{% url 'horizon:infrastructure:overview:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -1,25 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}provision_form{% endblock %}
{% block form_action %}{% url 'horizon:infrastructure:overview:undeploy_confirmation' %}{% endblock %}
{% block modal_id %}provision_modal{% endblock %}
{% block modal-header %}{% trans "Undeployment Confirmation" %}{% endblock %}
{% block modal-body %}
<div>
<p>{% trans "You are about undeploy your overcloud" %}
</p>
<p>{% trans "This operation cannot be undone. Are you sure you want to do that?" %}</p>
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary" type="submit" value="{% trans "Undeploy" %}" />
<a href="{% url 'horizon:infrastructure:overview:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends 'infrastructure/base.html' %}
{% load i18n %}
{% block title %}{% trans "Deploy overcloud" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Deploy overcloud") %}
{% endblock page_header %}
{% block infrastructure_main %}
{% include "infrastructure/overview/_deploy_confirmation.html" %}
{% endblock %}

View File

@ -1,22 +0,0 @@
{% load i18n %}
{% load url from future%}
<div class="deployment-icon">
{% block deployment-icon %}
<i class="fa fa-cloud text-info"></i>
{% endblock %}
</div>
<div class="deployment-box clearfix">
<h4>{% block deployment-title %}{% endblock %}</h4>
{% block deployment-info %}{% endblock %}
<div class="deployment-buttons clearfix">
{% block deployment-buttons %}
<a
href="{% url 'horizon:infrastructure:overview:undeploy_confirmation' %}"
class="btn btn-danger ajax-modal">
<i class="fa fa-trash"></i>
{% trans "Undeploy" %}
</a>
{% endblock %}
</div>
</div>

View File

@ -1,44 +0,0 @@
{% extends "infrastructure/overview/deployment_base.html" %}
{% load i18n %}
{% load url from future%}
{% block deployment-icon %}
<i class="fa fa-exclamation-circle text-danger"></i>
{% endblock %}
{% block deployment-title %}
{% if stack.is_delete_failed %}
{% trans "Undeploying failed" %}
{% elif stack.is_failed %}
{% trans "Deployment failed" %}
{% else %}
{% trans "Failure" %}
{% endif %}
{% endblock %}
{% block deployment-info %}
{% if last_failed_events %}
<strong>{% trans "Last failed events:" %}</strong>
{% for event in last_failed_events %}
<div>
<dl>
<dt>{% trans "Timestamp" %}</dt>
<dd><time datetime="{{ event.event_time }}">{{ event.event_time }}</time></dd>
<dt>{% trans "Resource Name" %}</dt>
<dd>{{ event.resource_name }}</dd>
<dt>{% trans "Status" %}</dt>
<dd>{{ event.resource_status }}</dd>
<dt>{% trans "Reason" %}</dt>
<dd>{{ event.resource_status_reason }}</dd>
</dl>
</div>
{% endfor %}
{% endif %}
<p><a href="{% url 'horizon:infrastructure:history:index' %}">See full log</a></p>
{% endblock %}
{% block deployment-buttons %}
{{ block.super }}
{% endblock %}

View File

@ -1,23 +0,0 @@
{% extends "infrastructure/overview/deployment_base.html" %}
{% load i18n %}
{% load url from future%}
{% block deployment-icon %}
<i class="fa fa-exclamation-triangle text-warning"></i>
{% endblock %}
{% block deployment-title %}{% trans "Initialization" %}{% endblock %}
{% block deployment-info %}
<p>{% trans "Your OpenStack cloud is deployed but it needs to get initialized in order to get live." %}</p>
{% endblock %}
{% block deployment-buttons %}
{{ block.super }}
<a href="{% url 'horizon:infrastructure:overview:post_deploy_init' %}"
class="btn btn-primary ajax-modal">
<i class="fa fa-flag-checkered"></i>
{% trans "Initialize" %}
</a>
{% endblock %}

View File

@ -1,33 +0,0 @@
{% extends "infrastructure/overview/deployment_base.html" %}
{% load i18n %}
{% load url from future%}
{% block deployment-icon %}
<i class="fa fa-check-circle-o text-success"></i>
{% endblock %}
{% block deployment-title %}{% trans "Deployment is live" %}{% endblock %}
{% block deployment-info %}
<strong>{% trans "Access information" %}</strong>
<dl>
{% for dashboard_url in dashboard_urls %}
<dt>{% trans "Horizon URL" %}</dt>
<dd><a href="{{ dashboard_url }}">{{ dashboard_url }}</a></dd>
{% endfor %}
<dt>{% trans "User name" %}</dt>
<dd>admin</dd>
<dt></dt>
<dd>
<form>
<fieldset>
<div class="form-group">
<label class="control-label required" for="id_password">{% trans "Password" %}</label>
<div>
<input class="form-control" id="id_password" type="password" value="{{ admin_password }}" disabled="true" readonly="true"/>
</div>
</div>
</fieldset>
</form>
</dd>
</dl>
{% endblock %}

View File

@ -1,43 +0,0 @@
{% extends "infrastructure/overview/deployment_base.html" %}
{% load i18n %}
{% load url from future%}
{% block deployment-icon %}
{% if plan_invalid %}
<i class="fa fa-exclamation-circle text-danger"></i>
{% else %}
<i class="fa fa-check-circle text-success"></i>
{% endif %}
{% endblock %}
{% block deployment-title %}
{% if plan_invalid %}
{% trans "Design your deployment" %}
{% else %}
{% trans "Ready to get deployed" %}
{% endif %}
{% endblock %}
{% block deployment-info %}
<ul class="list-unstyled">
{% for message in plan_messages %}
<li class="{% if message.is_critical %}text-danger{% else %}text-warning{% endif %}"><p>
{{ message.text }}
{% if message.link_url %}
<a href="{{ message.link_url }}">
{{ message.link_label|default:message.link_url }}
</a>
{% endif %}
</p></li>
{% endfor %}
</ul>
{% endblock %}
{% block deployment-buttons %}
<a href="{% url 'horizon:infrastructure:overview:deploy_confirmation' %}"
class="btn btn-primary ajax-modal btn-default {% if plan_invalid %}disabled{% endif %}">
<i class="fa fa-rocket"></i>
{% trans "Deploy" %}
</a>
{% endblock %}

View File

@ -1,61 +0,0 @@
{% extends "infrastructure/overview/deployment_base.html" %}
{% load i18n %}
{% load url from future%}
{% block deployment-icon %}
<i class="fa fa-spinner text-info"></i>
{% endblock %}
{% block deployment-title %}
{% if stack.is_deleting %}
{% trans "Undeploying..." %}
{% elif stack.is_deploying %}
{% trans "Deploying..." %}
{% endif %}
{% endblock %}
{% block deployment-info %}
{% if progress %}
<div class="progress">
<div
class="progress-bar progress-bar-striped active"
role="progressbar"
aria-valuenow="{{ progress }}"
aria-valuemin="0"
aria-valuemax="100"
style="width: {{ progress }}%"
><span class="sr-only">{{ progress }}% {% trans "Complete" %}</span></div>
</div>
{% endif %}
{% if last_failed_events %}
<strong>{% trans "Last failed events:" %}</strong>
{% for event in last_failed_events %}
<div>
<dl>
<dt>{% trans "Timestamp" %}</dt>
<dd><time datetime="{{ event.event_time }}">{{ event.event_time }}</time></dd>
<dt>{% trans "Resource Name" %}</dt>
<dd>{{ event.resource_name }}</dd>
<dt>{% trans "Status" %}</dt>
<dd>{{ event.resource_status }}</dd>
<dt>{% trans "Reason" %}</dt>
<dd>{{ event.resource_status_reason }}</dd>
</dl>
</div>
{% endfor %}
{% endif %}
<p><a href="{% url 'horizon:infrastructure:history:index' %}">See full log</a></p>
{% endblock %}
{% block deployment-buttons %}
{% if stack.is_deploying %}
<a
href="{% url 'horizon:infrastructure:overview:undeploy_confirmation' %}"
class="btn btn-danger ajax-modal">
<i class="fa fa-close"></i>
{% trans "Stop" %}
</a>
{% endif %}
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends 'infrastructure/base.html' %}
{% load i18n %}
{% block title %}{% trans "Initialize" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Initialize Overcloud") %}
{% endblock page_header %}
{% block infrastructure_main %}
{% include "infrastructure/overview/_post_deploy_init.html" %}
{% endblock %}

View File

@ -1,32 +0,0 @@
{% load i18n %}
{% load url from future %}
{% load form_helpers %}
<h4>{% trans "Deployment Roles" %}</h4>
<form method="POST" action="." class="deployment-roles-form">
{% csrf_token %}
{% include 'horizon/common/_form_errors.html' with form=form %}
{% for role in roles %}
<div class="form-group well well-sm clearfix{% if field.errors %} error{% endif %} {{ field.css_classes }}">
<div class="col-sm-2">
{{ role.field|add_bootstrap_class }}
</div>
<div class="col-sm-10">
<a
href="{% url "horizon:infrastructure:roles:detail" role_id=role.id %}"
class="deployment-roles-label"
>{{ role.name }}</a>
{% for error in role.field.errors %}
<span class="help-block"><span class="text-danger">
{{ error }}
</span></span>
{% endfor %}
</div>
</div>
{% endfor %}
<hr>
<button type="submit" class="btn btn-default">
<i class="fa fa-save"></i>
{% trans "Save changes" %}
</button>
</form>

View File

@ -1,28 +0,0 @@
{% load i18n %}
{% load url from future %}
<h4>{% trans "Deployment Roles" %}</h4>
{% for role in roles %}
<div class="alert well-sm clearfix
{% if role.error_node_count %}
alert-danger
{% elif role.deployed_node_count == role.planned_node_count %}
alert-success
{% else %}
alert-info
{% endif %}
">
<div class="col-sm-2">
{% if role.deployed_node_count < role.planned_node_count %}
<strong>{{ role.deployed_node_count }}</strong><small class="text-muted">/{{ role.planned_node_count }}</small>
{% else %}
<strong>{{ role.planned_node_count }}</strong>
{% endif %}
</div>
<div class="col-sm-10">
<a
href="{% url "horizon:infrastructure:roles:detail" role_id=role.id %}"
>{{ role.name }}</a>
</div>
</div>
{% endfor %}

View File

@ -1,11 +0,0 @@
{% extends 'infrastructure/base.html' %}
{% load i18n %}
{% block title %}{% trans "Undeploy overcloud" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Undeploy overcloud") %}
{% endblock page_header %}
{% block infrastructure_main %}
{% include "infrastructure/overview/_undeploy_confirmation.html" %}
{% endblock %}

View File

@ -15,12 +15,14 @@
from django.conf import urls
from tuskar_ui.infrastructure.overview import views
import tuskar_boxes.views
import tuskar_boxes.overview.views as boxes_views
urlpatterns = urls.patterns(
'',
urls.url(r'^$', tuskar_boxes.views.IndexView.as_view(), name='index'),
urls.url(r'^$',
boxes_views.IndexView.as_view(),
name='index'),
urls.url(r'^deploy-confirmation$',
views.DeployConfirmationView.as_view(),
name='deploy_confirmation'),

View File

@ -12,7 +12,40 @@
# License for the specific language governing permissions and limitations
# under the License.
from tuskar_ui import api
from tuskar_ui.infrastructure.overview import views
def flavor_nodes(request, flavor):
"""Lists all nodes that match the given flavor exactly."""
for node in api.node.Node.list(request):
if all([
int(node.cpus) == int(flavor.vcpus),
int(node.memory_mb) == int(flavor.ram),
int(node.local_gb) == int(flavor.disk),
# TODO(rdopieralski) add architecture when available
]):
yield node
class IndexView(views.IndexView):
pass
template_name = "tuskar_boxes/overview/index.html"
def get_context_data(self, *args, **kwargs):
context = super(IndexView, self).get_context_data(*args, **kwargs)
flavors = api.flavor.Flavor.list(self.request)
flavors.sort(key=lambda np: (np.vcpus, np.ram, np.disk))
context['flavors'] = []
for flavor in flavors:
nodes = [{
'role': '',
} for node in flavor_nodes(self.request, flavor)]
flavor = {
'name': flavor.name,
'vcpus': flavor.vcpus,
'ram': flavor.ram,
'disk': flavor.disk,
'nodes': nodes,
}
if nodes: # Don't list empty flavors
context['flavors'].append(flavor)
return context

View File

@ -4,13 +4,6 @@
{% block title %}{% trans 'My OpenStack Deployment' %}{% endblock %}
{% block css %}
{% if stack.is_deploying %}
<meta http-equiv="refresh" content="30">
{% endif %}
{{ block.super }}
{% endblock %}
{% block page_header %}
{% include 'horizon/common/_domain_page_header.html' with title=_('My OpenStack Deployment') %}
{% endblock page_header %}
@ -36,7 +29,7 @@
{% if stack %}
{% include "infrastructure/overview/role_nodes_status.html" %}
{% else %}
{% include "infrastructure/overview/role_nodes_edit.html" %}
{% include "tuskar_boxes/overview/role_nodes_edit.html" %}
{% endif %}
</div>
</div>

View File

@ -0,0 +1,205 @@
{% load i18n %}
{% load url from future %}
{% load form_helpers %}
<h4>{% trans "Available Deployment Roles" %}</h4>
<form method="POST" action="." class="boxes-form">
{% csrf_token %}
{% include 'horizon/common/_form_errors.html' with form=form %}
<div class="boxes-available-roles">
{% for role in roles %}{% spaceless %}
<div class="boxes-role-{{ role.name|slugify }} boxes-role" data-name="{{ role.name|slugify }}">
{{ role.field|add_bootstrap_class }}
{{ role.name|title }}
</div>
{% endspaceless %}{% endfor %}
</div>
<h4>{% trans "Available Node Profiles" %}</h4>
{% for flavor in flavors %}
<div class="boxes-flavor">
<p>
<strong>Node Profile:</strong>
<i>{{ flavor.name }}</i>
{{ flavor.vcpus }} CPU,
{{ flavor.ram }}MB RAM,
{{ flavor.disk }}GB Disk
</p>
<div class="row">
<div class="col-xs-5">
<div class="boxes-drop-roles">
</div>
<div class="boxes-drop">
<i class="fa fa-plus"></i>
</div>
</div>
<div class="col-xs-7 boxes-nodes">
{% for node in flavor.nodes %}{% spaceless %}
<div class="boxes-node">free</div>
{% endspaceless %}{% endfor %}
</div>
</div>
<div>
{% endfor %}
<hr>
<button type="submit" class="btn btn-default">
<i class="fa fa-save"></i>
{% trans "Save changes" %}
</button>
</form>
<script type="text/javascript">
(window.$ || window.addHorizonLoadEvent)(function () {
function get_role_counts($flavor) {
var roles = {};
$flavor.find('div.boxes-drop-roles div.boxes-role').each(function () {
var $this = $(this);
var name = $this.data('name');
var count = +$this.find('input.number-picker').val();
roles[name] = count;
});
return roles;
}
function update_boxes() {
$('div.boxes-flavor').each(function () {
var $flavor = $(this);
var roles = get_role_counts($flavor);
var role_names = Object.getOwnPropertyNames(roles);
var count = 0;
var role = 0;
$flavor.find('div.boxes-nodes div.boxes-node').each(function () {
var $this = $(this);
$this.removeClass('boxes-role-controller boxes-role-compute boxes-role-block-storage boxes-role-object-storage');
while (count >= roles[role_names[role]]) {
role += 1;
count = 0;
}
if (!role_names[role]) {
$(this).html('free');
} else {
$this.addClass('boxes-role-' + role_names[role]).html('&nbsp;');
}
count += 1;
});
});
}
$('div.boxes-role').draggable({
revert: 'invalid',
helper: 'clone',
stack: '.boxes-roles',
opacity: 0.5
});
$('div.boxes-drop').droppable({
accept: 'div.boxes-role',
activeClass: 'boxes-drop-active',
hoverClass: 'boxes-drop-hover',
tolerance: 'touch',
drop: function (ev, ui) {
ui.draggable.appendTo($(this).prev('.boxes-drop-roles'));
ui.draggable.find('input.number-picker').val(1);
window.setTimeout(update_boxes, 0);
}
});
$('div.boxes-available-roles').droppable({
accept: 'div.boxes-role',
activeClass: 'boxes-drop-active',
hoverClass: 'boxes-drop-hover',
tolerance: 'touch',
drop: function (ev, ui) {
ui.draggable.appendTo(this);
ui.draggable.find('input.number-picker').val(0);
window.setTimeout(update_boxes, 0);
}
});
update_boxes();
$('input.number-picker').change(update_boxes);
});
</script>
<style type="text/css">
.boxes-node {
display: inline-block;
width: 60px;
height: 60px;
border-radius: 2px;
border: 1px solid #999;
background: #eee;
margin: 0 4px 4px 0;
text-align: center;
color: #666;
padding: 20px 4px 0 4px;
}
.boxes-available-roles {
border-radius: 2px;
background: #eee;
border: 1px dashed #666;
min-height: 42px;
min-width: 120px;
display: inline-block;
padding: 4px 0 0 4px;
}
.boxes-role {
display: inline-block;
padding: 6px;
border: 1px solid;
width: 180px;
cursor: move;
border-radius: 2px;
margin: 0 4px 4px 0;
background-color: #fce94f;
border-color: #edd400;
color: #c4a000;
}
.boxes-available-roles .boxes-role {
text-align: center;
width: 120px;
}
.boxes-available-roles .boxes-role .form-control {
display: none;
}
.boxes-role-controller {
background-color: #fcaf3e;
border-color: #f57900;
color: #ce5c00;
}
.boxes-role-compute {
background-color: #8ae234;
border-color: #73d216;
color: #4e9a06;
}
.boxes-role-object-storage {
background-color: #729fcf;
border-color: #3465a4;
color: #204a87;
}
.boxes-role-block-storage {
background-color: #ad7fa8;
border-color: #75507b;
color: #5c3566;
}
.boxes-drop {
display: inline-block;
padding: 6px;
border: 1px dashed;
width: 180px;
text-align: center;
border-radius: 2px;
background-color: #eee;
border-color: #666;
color: #444;
}
.boxes-drop-active {
background-color: #ccc;
border-style: solid;
}
.boxes-drop-hover {
background-color: #999;
border-style: solid;
}
</style>