Add draggable components to the 'Components page'
* Add components tiles organized via bootstrap carousel widget to the environment page * Make these components draggable, drop area resides inside the components table * Enhance components list with filter field and categories selector which perform components filtering client-side Copy-paste the code for loading modals from horizon.modals.js into static/muranodashboard/js/load-modals.js to not rely on change in Horizon. Implements blueprint draggable-components Change-Id: I71935db1c5f193441e4a0df54362de7ee2bafca0
This commit is contained in:

committed by
Ekaterina Chernova

parent
97531bb34e
commit
6624d47ca0
@@ -85,6 +85,16 @@ def get_environments_context(request):
|
||||
return context
|
||||
|
||||
|
||||
def get_categories_list(request):
|
||||
categories = []
|
||||
with api.handled_exceptions(request):
|
||||
client = api.muranoclient(request)
|
||||
categories = client.packages.categories()
|
||||
if ALL_CATEGORY_NAME not in categories:
|
||||
categories.insert(0, ALL_CATEGORY_NAME)
|
||||
return categories
|
||||
|
||||
|
||||
@auth_dec.login_required
|
||||
def switch(request, environment_id,
|
||||
redirect_field_name=auth.REDIRECT_FIELD_NAME):
|
||||
@@ -509,15 +519,8 @@ class IndexView(list_view.ListView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(IndexView, self).get_context_data(**kwargs)
|
||||
|
||||
categories = []
|
||||
with api.handled_exceptions(self.request):
|
||||
client = api.muranoclient(self.request)
|
||||
categories = client.packages.categories()
|
||||
if ALL_CATEGORY_NAME not in categories:
|
||||
categories.insert(0, ALL_CATEGORY_NAME)
|
||||
|
||||
context.update({
|
||||
'categories': categories,
|
||||
'categories': get_categories_list(self.request),
|
||||
'current_category': self.get_current_category(),
|
||||
'latest_list': clean_latest_apps(self.request)
|
||||
})
|
||||
|
@@ -12,6 +12,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
@@ -22,9 +23,13 @@ from horizon import exceptions
|
||||
from horizon import messages
|
||||
from horizon import tables
|
||||
|
||||
from muranodashboard.catalog import views as catalog_views
|
||||
from muranodashboard.environments import api
|
||||
from muranodashboard.environments import consts
|
||||
|
||||
from muranodashboard import api as api_utils
|
||||
from muranodashboard.api import packages as pkg_api
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -273,10 +278,26 @@ class ServicesTable(tables.DataTable):
|
||||
def get_object_id(self, datum):
|
||||
return datum['?']['id']
|
||||
|
||||
def get_apps_list(self):
|
||||
packages = []
|
||||
with api_utils.handled_exceptions(self.request):
|
||||
packages, self._more = pkg_api.package_list(
|
||||
self.request, filters={'type': 'Application'})
|
||||
return json.dumps([package.to_dict() for package in packages])
|
||||
|
||||
def actions_allowed(self):
|
||||
status, version = _get_environment_status_and_version(
|
||||
self.request, self)
|
||||
return status not in consts.NO_ACTION_ALLOWED_STATUSES
|
||||
|
||||
def get_categories_list(self):
|
||||
return catalog_views.get_categories_list(self.request)
|
||||
|
||||
class Meta:
|
||||
name = 'services'
|
||||
verbose_name = _('Components')
|
||||
template = 'common/_data_table.html'
|
||||
verbose_name = _('Component List')
|
||||
template = 'services/_data_table.html'
|
||||
no_data_message = _('NO COMPONENTS')
|
||||
status_columns = ['status']
|
||||
row_class = UpdateServiceRow
|
||||
table_actions = (AddApplication, DeployThisEnvironment)
|
||||
|
@@ -76,9 +76,125 @@ h3.heading_switcher a.btn {
|
||||
padding: 5px 0;
|
||||
}
|
||||
.centering {
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.draggable_app img {
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.draggable_app .well {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 916px) {
|
||||
.draggable_app .well, .no_apps.well {
|
||||
min-height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 916px) and (max-width: 1158px) {
|
||||
.draggable_app .well, .no_apps.well {
|
||||
min-height: 105px;
|
||||
}
|
||||
}
|
||||
|
||||
.has-feedback .form-control-feedback {
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.draggable_app {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.draggable_app.col-xs-2 {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.carousel-inner .row {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.carousel {
|
||||
padding: 0 40px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.carousel-control {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: -10px;
|
||||
bottom: 0;
|
||||
width: 30px;
|
||||
opacity: 1;
|
||||
filter: alpha(opacity=100);
|
||||
font-size: 20px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.carousel-control:hover:focus {
|
||||
color: #d93c27;
|
||||
}
|
||||
|
||||
.carousel-control:hover {
|
||||
color: #d93c27;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.carousel-control:focus {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.carousel-control.left {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.carousel-control.right {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.extra_title {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.draggable_app .well:hover {
|
||||
border-color: #d93c27;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#envAppsCategory > button{
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#envAppsCategory > h4 {
|
||||
position: absolute;
|
||||
left: -110px;
|
||||
top: 7px;
|
||||
}
|
||||
|
||||
#envAppsFilter {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
div.drop_component {
|
||||
border: 2px #ccc dashed;
|
||||
width: 85%;
|
||||
margin: 0 auto -2px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
div.drop_component.over {
|
||||
background-color: #ffffc0;
|
||||
border-color: #e8c502;
|
||||
}
|
||||
|
||||
h3.link {
|
||||
color: yellow;
|
||||
opacity: 0.5;
|
||||
|
@@ -0,0 +1,136 @@
|
||||
$(function() {
|
||||
horizon.tabs.addTabLoadFunction(initServicesTab);
|
||||
initServicesTab($('.tab-content .tab-pane.active'));
|
||||
function initServicesTab($tab) {
|
||||
var $dropArea = $tab.find('.drop_component'),
|
||||
draggedAppUrl = null,
|
||||
firstDropTarget = null;
|
||||
|
||||
function bindAppTileHandlers() {
|
||||
$('.draggable_app').each(function () {
|
||||
$(this).on('dragstart', function (ev) {
|
||||
ev.originalEvent.dataTransfer.effectAllowed = 'copy';
|
||||
// we have to use an external variable for this since
|
||||
// storing data in dataTransfer object works only for FF
|
||||
draggedAppUrl = $(this).find('input[type="hidden"]').val();
|
||||
// set it so the DND works in FF
|
||||
ev.originalEvent.dataTransfer.setData('text/uri-list', draggedAppUrl);
|
||||
}).on('dragend', function () {
|
||||
$dropArea.removeClass('over');
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
$dropArea.on('dragover', function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.originalEvent.dataTransfer.dropEffect = 'copy';
|
||||
return false;
|
||||
}).on('dragenter', function (ev) {
|
||||
$dropArea.addClass('over');
|
||||
firstDropTarget = ev.target;
|
||||
}).on('dragleave', function (ev) {
|
||||
if (firstDropTarget === ev.target) {
|
||||
$dropArea.removeClass('over');
|
||||
}
|
||||
}).on('drop', function (ev) {
|
||||
ev.preventDefault();
|
||||
horizon.modals.loadModal(draggedAppUrl);
|
||||
return false;
|
||||
});
|
||||
var packages = $.parseJSON($('#apps_carousel_contents').val());
|
||||
|
||||
function subdivide(numOfItems) {
|
||||
var chunks = [],
|
||||
seq = this,
|
||||
head = seq.slice(0, numOfItems),
|
||||
tail = seq.slice(numOfItems);
|
||||
while (tail.length) {
|
||||
chunks.push(head);
|
||||
head = tail.slice(0, numOfItems);
|
||||
tail = tail.slice(numOfItems);
|
||||
}
|
||||
chunks.push(head);
|
||||
return chunks;
|
||||
}
|
||||
|
||||
Array.prototype.subdivide = subdivide;
|
||||
var $carouselInner = $tab.find('.carousel-inner'),
|
||||
$carousel = $('#apps_carousel'),
|
||||
$filter = $('#envAppsFilter').find('input'),
|
||||
category = ALL_CATEGORY = 'All',
|
||||
filterValue = '',
|
||||
ENTER_KEYCODE = 13;
|
||||
var tileTemplate = Hogan.compile($('#app_tile_small').html()),
|
||||
environmentId = $('#environmentId').val();
|
||||
|
||||
function fillCarousel(apps) {
|
||||
if (apps.length) {
|
||||
apps.subdivide(6).forEach(function (chunk, index) {
|
||||
var $item = $('<div class="item"></div>'),
|
||||
$row = $('<div class="row"></div>');
|
||||
if (index == 0) {
|
||||
$item.addClass('active');
|
||||
}
|
||||
$item.appendTo($row);
|
||||
chunk.forEach(function (pkg) {
|
||||
var html = tileTemplate.render({
|
||||
app_name: pkg.name,
|
||||
environment_id: environmentId,
|
||||
app_id: pkg.id
|
||||
});
|
||||
$(html).appendTo($item);
|
||||
});
|
||||
$item.appendTo($carouselInner);
|
||||
});
|
||||
$('div.carousel-control').removeClass('item')
|
||||
$carousel.show();
|
||||
bindAppTileHandlers();
|
||||
} else {
|
||||
$carousel.hide();
|
||||
}
|
||||
}
|
||||
|
||||
if (packages) {
|
||||
fillCarousel(packages);
|
||||
}
|
||||
$carousel.carousel({interval: false});
|
||||
function refillCarousel() {
|
||||
$carouselInner.empty();
|
||||
if (category === ALL_CATEGORY && filterValue === '') {
|
||||
fillCarousel(packages);
|
||||
} else {
|
||||
var filterRegexp = new RegExp(filterValue, 'i'),
|
||||
filterRegexpExact = new RegExp('\\b' + filterValue + '\\b', 'i');
|
||||
fillCarousel(packages.filter(function (pkg) {
|
||||
var categorySatisfied = true, filterSatisfied = true;
|
||||
if (category !== ALL_CATEGORY) {
|
||||
categorySatisfied = pkg.categories.indexOf(category) > -1;
|
||||
}
|
||||
if (filterValue !== '') {
|
||||
filterSatisfied = pkg.name.match(filterRegexp);
|
||||
filterSatisfied = filterSatisfied || pkg.description.match(filterRegexp);
|
||||
filterSatisfied = filterSatisfied || pkg.tags.some(function (tag) {
|
||||
return tag.match(filterRegexpExact);
|
||||
});
|
||||
}
|
||||
return categorySatisfied && filterSatisfied;
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// dynamic carousel refilling on category change
|
||||
$('#envAppsCategory').on('click', 'a', function (env) {
|
||||
category = $(this).text();
|
||||
$('#envAppsCategoryName').text(category);
|
||||
refillCarousel();
|
||||
});
|
||||
// dynamic carousel refilling on search box non-empty submission
|
||||
$filter.keypress(function (ev) {
|
||||
if (ev.which == ENTER_KEYCODE) {
|
||||
filterValue = $filter.val();
|
||||
refillCarousel();
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
49
muranodashboard/static/muranodashboard/js/load-modals.js
Normal file
49
muranodashboard/static/muranodashboard/js/load-modals.js
Normal file
@@ -0,0 +1,49 @@
|
||||
$(function() {
|
||||
|
||||
horizon.modals.loadModal = function (url, updateFieldId) {
|
||||
// If there's an existing modal request open, cancel it out.
|
||||
if (horizon.modals._request && typeof(horizon.modals._request.abort) !== undefined) {
|
||||
horizon.modals._request.abort();
|
||||
}
|
||||
|
||||
horizon.modals._request = $.ajax(url, {
|
||||
beforeSend: function () {
|
||||
horizon.modals.modal_spinner(gettext("Loading"));
|
||||
},
|
||||
complete: function () {
|
||||
// Clear the global storage;
|
||||
horizon.modals._request = null;
|
||||
horizon.modals.spinner.modal('hide');
|
||||
},
|
||||
error: function(jqXHR, status, errorThrown) {
|
||||
if (jqXHR.status === 401){
|
||||
var redir_url = jqXHR.getResponseHeader("X-Horizon-Location");
|
||||
if (redir_url){
|
||||
location.href = redir_url;
|
||||
} else {
|
||||
location.reload(true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!horizon.ajax.get_messages(jqXHR)) {
|
||||
// Generic error handler. Really generic.
|
||||
horizon.alert("danger", gettext("An error occurred. Please try again later."));
|
||||
}
|
||||
}
|
||||
},
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
var update_field_id = updateFieldId,
|
||||
modal,
|
||||
form;
|
||||
modal = horizon.modals.success(data, textStatus, jqXHR);
|
||||
if (update_field_id) {
|
||||
form = modal.find("form");
|
||||
if (form.length) {
|
||||
form.attr("data-add-to-field", update_field_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
});
|
@@ -4,7 +4,6 @@
|
||||
{% block table_caption %}
|
||||
<tr class='table_caption'>
|
||||
<th class='table_header' colspan='{{ columns|length }}'>
|
||||
{# <h3 class='table_title'>{{ table }}</h3> #}
|
||||
{{ table.render_table_actions }}
|
||||
</th>
|
||||
</tr>
|
||||
|
79
muranodashboard/templates/services/_data_table.html
Normal file
79
muranodashboard/templates/services/_data_table.html
Normal file
@@ -0,0 +1,79 @@
|
||||
{% extends 'common/_data_table.html' %}
|
||||
{% load i18n %}
|
||||
{% load custom_filters %}
|
||||
{% block title %}{% trans "Components" %}{% endblock %}
|
||||
|
||||
{% block table_caption %}
|
||||
{% if table.actions_allowed %}
|
||||
<div class="row">
|
||||
<div class="col-xs-4">
|
||||
<h3 class="table_title extra_title">{% trans "Application Components" %}</h3>
|
||||
</div>
|
||||
<div class="col-xs-offset-2 col-xs-3">
|
||||
<div class="dropdown" id="envAppsCategory">
|
||||
{% with categories=table.get_categories_list %}
|
||||
<h4>{% trans 'App category' %}</h4>
|
||||
<button class="btn btn-default dropdown-toggle"
|
||||
type="button" aria-expanded="true">
|
||||
<span id="envAppsCategoryName">{{ categories.0 }}</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="envAppsCategory">
|
||||
{% for category in categories %}
|
||||
<li role="presentation"><a role="menuitem" tabindex="-1" href="#">{{ category }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-3">
|
||||
<div class="has-feedback" id="envAppsFilter">
|
||||
<input class="form-control" value="{{ search }}" type="text"
|
||||
placeholder="Find in a selected category">
|
||||
<span class="fa fa-search form-control-feedback"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
{% with apps=table.get_apps_list %}
|
||||
{% if apps|length %}
|
||||
<div id="apps_carousel" class="carousel">
|
||||
<input type="hidden" id="apps_carousel_contents" value="{{ apps }}">
|
||||
<div class="carousel-inner"></div>
|
||||
<a class="left carousel-control" href="#apps_carousel" data-slide="prev">
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</a>
|
||||
<a class="right carousel-control" href="#apps_carousel" data-slide="next">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="well">
|
||||
<h4>No Application Components match the current category and/or filter string</h4>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
<tr class='table_caption'>
|
||||
<th class='table_header' colspan='{{ columns|length }}'>
|
||||
<h3 class='table_title'>{{ table }}</h3>
|
||||
{{ table.render_table_actions }}
|
||||
</th>
|
||||
</tr>
|
||||
{% endblock table_caption %}
|
||||
|
||||
{% block table_body %}
|
||||
<tbody>
|
||||
{% if table.actions_allowed %}
|
||||
<tr><td colspan="{{ table.get_columns|length }}">
|
||||
<div class="drop_component">
|
||||
<h4 class="centering"><i class="fa fa-bullseye"></i> {% trans 'Drop component here to add it' %}</h4></div>
|
||||
</td></tr>
|
||||
{% endif %}
|
||||
{% for row in rows %}
|
||||
{{ row.render }}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endblock table_body %}
|
12
muranodashboard/templates/services/app_tile_small.html
Normal file
12
muranodashboard/templates/services/app_tile_small.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
<div class="col-xs-2 draggable_app">
|
||||
<div class="well well-sm" draggable="true">
|
||||
<img class="centering" src="{% url 'horizon:murano:catalog:images' app.id %}"
|
||||
height="50" width="50" draggable="false"/>
|
||||
<input type="hidden" value="{% url 'horizon:murano:catalog:add' app.id environment_id 'True' 'True' %}"/>
|
||||
<div class="centering">{{ app.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,12 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load horizon %}
|
||||
{% load custom_filters %}
|
||||
{% block title %}{% trans "Components" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "services/_page_header.html" %}
|
||||
{% endblock page_header %}
|
||||
{% block main %}
|
||||
<input type="hidden" id="environmentId" value="{{ environment_id }}">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
|
||||
{{ tab_group.render }}
|
||||
@@ -17,4 +20,24 @@
|
||||
{% block css %}
|
||||
{% include "_stylesheets.html" %}
|
||||
<link rel="stylesheet" href="{% static 'muranodashboard/css/catalog.css' %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{% include 'horizon/_scripts.html' %}
|
||||
<script src="{% static 'muranodashboard/js/load-modals.js' %}"></script>
|
||||
<script type="text/template" id="app_tile_small">
|
||||
{% jstemplate %}
|
||||
{% url 'horizon:murano:catalog:images' '[[app_id]]' as image_url %}
|
||||
{% url 'horizon:murano:catalog:add' '[[app_id]]' '[[environment_id]]' 'True' 'True' as add_url %}
|
||||
<div class="col-xs-2 draggable_app">
|
||||
<div class="well well-sm" draggable="true">
|
||||
<img class="centering" src="{{ image_url|unquote }}"
|
||||
height="50" width="50" draggable="false"/>
|
||||
<input type="hidden" value="{{ add_url|unquote }}"/>
|
||||
<div class="centering">[[app_name]]</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endjstemplate %}
|
||||
</script>
|
||||
<script src="{% static 'muranodashboard/js/draggable-components.js' %}"></script>
|
||||
{% endblock %}
|
@@ -1,5 +1,7 @@
|
||||
from django import forms
|
||||
from django import template
|
||||
from django.template import defaultfilters
|
||||
import urllib
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -19,3 +21,9 @@ def first_half(seq):
|
||||
def last_half(seq):
|
||||
half_len = len(seq) / 2
|
||||
return seq[half_len:]
|
||||
|
||||
|
||||
@register.filter(name='unquote')
|
||||
@defaultfilters.stringfilter
|
||||
def unquote_raw(value):
|
||||
return urllib.unquote(value)
|
||||
|
Reference in New Issue
Block a user