Commit 744c1885 by Your Name

storage + UI fix

parent 9b3516eb
......@@ -24,7 +24,7 @@ import pyotp
from crispy_forms.bootstrap import FormActions
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (
Layout, Div, BaseInput, Field, HTML, Submit, TEMPLATE_PACK, Fieldset
Layout, Div, BaseInput, Field, HTML, Submit, TEMPLATE_PACK, Fieldset,
)
from crispy_forms.utils import render_field
from dal import autocomplete
......@@ -1735,24 +1735,74 @@ class DataStoreForm(ModelForm):
fields = ("name", "path", "hostname")
#class DiskForm(ModelForm):
# created = forms.DateTimeField()
# modified = forms.DateTimeField()
#
# def __init__(self, *args, **kwargs):
# super(DiskForm, self).__init__(*args, **kwargs)
#
# for k, v in self.fields.iteritems():
# v.widget.attrs['readonly'] = True
# self.fields['created'].initial = self.instance.created
# self.fields['modified'].initial = self.instance.modified
#
# class Meta:
# model = Disk
# fields = ("name", "filename", "datastore", "type", "bus", "size",
# "base", "dev_num", "destroyed", "is_ready",)
#
class DiskForm(ModelForm):
created = forms.DateTimeField()
modified = forms.DateTimeField()
created = forms.DateTimeField(required=False)
modified = forms.DateTimeField(required=False)
@property
def helper(self):
helper = FormHelper()
helper.form_method = "post"
helper.layout = Layout(
Fieldset(
'',
'name',
'filename',
'datastore',
'type',
'bus',
'size',
'base',
'dev_num',
'destroyed',
'is_ready',
'created',
'modified',
),
FormActions(
Submit('submit', 'Save'),
)
)
return helper
def __init__(self, *args, **kwargs):
super(DiskForm, self).__init__(*args, **kwargs)
for k, v in self.fields.iteritems():
v.widget.attrs['readonly'] = True
self.fields['created'].initial = self.instance.created
self.fields['modified'].initial = self.instance.modified
# Make all fields non-editable except 'datastore', and 'filename'.
for name, field in self.fields.iteritems():
if name == 'datastore' or name == 'filename':
continue
# field.widget.attrs['disabled'] = 'disabled'
# Show timestamps (read-only display fields)
self.fields['created'].initial = getattr(self.instance, 'created', None)
self.fields['modified'].initial = getattr(self.instance, 'modified', None)
# self.fields['created'].widget.attrs['disabled'] = 'disabled'
# self.fields['modified'].widget.attrs['disabled'] = 'disabled'
class Meta:
model = Disk
fields = ("name", "filename", "datastore", "type", "bus", "size",
"base", "dev_num", "destroyed", "is_ready",)
class MessageForm(ModelForm):
class Meta:
model = Message
......
......@@ -27,39 +27,43 @@ $(function() {
return false;
});
function initAutocompleteSelect2($root) {
$root.find('select[data-autocomplete-light-function="select2"]').each(function () {
var $el = $(this);
// már initelve?
if ($el.hasClass('select2-hidden-accessible')) return;
var url = $el.data('autocomplete-light-url');
var placeholder = $el.data('placeholder') || '';
$el.select2({
dropdownParent: $root, // bootstrap modal fókusz miatt
width: 'resolve',
placeholder: placeholder,
allowClear: true,
ajax: {
url: url,
dataType: 'json',
delay: 250,
data: function (params) {
return { q: params.term, page: params.page || 1 };
},
processResults: function (data) {
// DAL autocomplete view tipikusan már {results: [...], pagination: {more: ...}}
return data;
}
},
// data-html="true" miatt: hagyjuk az HTML-t renderelődni
escapeMarkup: function (m) { return m; }
});
});
function spinDisks() {
$('#disks-spinner').show().addClass('fa-spin');
}
$(function () {
$('#storage-link').on('click', function () {
$('#storage-link-spinner')
.show()
.addClass('fa-spin');
});
});
// Datastore autosubmit spinner
$('select[data-autosubmit="1"]').on('change', function () {
$('#ds-spinner')
.show()
.addClass('fa-spin');
});
// Filter clicks
$('.storage-filter').on('click', function () {
spinDisks();
});
// Search submit
$('#network-host-list-form').on('submit', function () {
spinDisks();
});
$(function () {
$('.nav-spinner').on('click', function () {
$(this).find('.fa-spinner')
.show()
.addClass('fa-spin');
});
});
function showConfirmationModal(data) {
// ha valamiért bent maradt egy régi modal, takarítsuk (örökölt kódnál előfordul)
$('#confirmation-modal').remove();
......
......@@ -29,39 +29,6 @@ $(function () {
return false;
});
function initAutocompleteSelect2($root) {
$root.find('select[data-autocomplete-light-function="select2"]').each(function () {
var $el = $(this);
// már initelve?
if ($el.hasClass('select2-hidden-accessible')) return;
var url = $el.data('autocomplete-light-url');
var placeholder = $el.data('placeholder') || '';
$el.select2({
dropdownParent: $root, // bootstrap modal fókusz miatt
width: 'resolve',
placeholder: placeholder,
allowClear: true,
ajax: {
url: url,
dataType: 'json',
delay: 250,
data: function (params) {
return { q: params.term, page: params.page || 1 };
},
processResults: function (data) {
// DAL autocomplete view tipikusan már {results: [...], pagination: {more: ...}}
return data;
}
},
// data-html="true" miatt: hagyjuk az HTML-t renderelődni
escapeMarkup: function (m) { return m; }
});
});
}
$('.group-create, .group-import, .group-export, .node-create, .tx-tpl-ownership, .group-delete, .node-delete, .disk-remove, .template-delete, .delete-from-group, .group-remove-all-btn, .lease-delete').click(function(e) {
$.ajax({
type: 'GET',
......@@ -317,6 +284,39 @@ $(function () {
});
});
function initAutocompleteSelect2($root) {
$root.find('select[data-autocomplete-light-function="select2"]').each(function () {
var $el = $(this);
// már initelve?
if ($el.hasClass('select2-hidden-accessible')) return;
var url = $el.data('autocomplete-light-url');
var placeholder = $el.data('placeholder') || '';
$el.select2({
dropdownParent: $root, // bootstrap modal fókusz miatt
width: 'resolve',
placeholder: placeholder,
allowClear: true,
ajax: {
url: url,
dataType: 'json',
delay: 250,
data: function (params) {
return { q: params.term, page: params.page || 1 };
},
processResults: function (data) {
// DAL autocomplete view tipikusan már {results: [...], pagination: {more: ...}}
return data;
}
},
// data-html="true" miatt: hagyjuk az HTML-t renderelődni
escapeMarkup: function (m) { return m; }
});
});
}
function generateVmHTML(data, is_last) {
return '<a href="' + data.url + '" class="list-group-item' +
(is_last ? ' list-group-item-last' : '') + '">' +
......
......@@ -99,100 +99,5 @@
{% block extra_etc %}
{% endblock %}
<style>
/* Full-page loading overlay */
#loading-overlay {
position: fixed;
top: 0; left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.85);
z-index: 9999;
display: none;
}
#loading-overlay .spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #555;
}
</style>
<div id="loading-overlay">
<div class="spinner">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<p>{% trans "Loading..." %}</p>
</div>
</div>
// <script>
// (function () {
// // Robust loader without beforeunload/select-change pitfalls.
// var overlay = document.getElementById("loading-overlay");
// if (!overlay) return;
//
// var timer = null;
//
// function showLoaderDelayed() {
// // Avoid stacking timers.
// if (timer) return;
//
// timer = setTimeout(function () {
// overlay.style.display = "block";
// }, 300); // show only if navigation takes noticeable time
// }
//
// function cancelLoader() {
// if (timer) {
// clearTimeout(timer);
// timer = null;
// }
// // In case it was shown and the browser restored from BFCache.
// overlay.style.display = "none";
// }
//
// // Cancel on load/pageshow (covers bfcache restore too).
// window.addEventListener("load", cancelLoader);
// window.addEventListener("pageshow", cancelLoader);
//
// // Show on form submit (POST or GET).
// var forms = document.getElementsByTagName("form");
// for (var i = 0; i < forms.length; i++) {
// forms[i].addEventListener("submit", function () {
//// showLoaderDelayed();
// });
// }
// // Show on autosubmit select change (programmatic form.submit() does not trigger submit events).
// var autos = document.querySelectorAll('select[data-autosubmit="1"]');
// for (var k = 0; k < autos.length; k++) {
// autos[k].addEventListener("change", function () {
// showLoaderDelayed();
// });
// }
//
// // Show on same-tab link clicks.
// var links = document.getElementsByTagName("a");
// for (var j = 0; j < links.length; j++) {
// (function (a) {
// a.addEventListener("click", function (e) {
// // Ignore modified clicks (new tab/window, etc.)
// if (e.ctrlKey || e.shiftKey || e.metaKey || e.altKey) return;
// if (a.target && a.target !== "_self") return;
// if (!a.href) return;
//
// // Ignore hash-only navigation on the same page.
// var href = a.getAttribute("href");
// if (href && href.charAt(0) === "#") return;
//
//// showLoaderDelayed();
// });
// })(links[j]);
// }
// })();
// </script>
</body>
</html>
......@@ -38,9 +38,10 @@
</a>
</li>
<li>
<a href="{% url "dashboard.views.storage" %}">
<a href="{% url "dashboard.views.storage" %}" class="nav-spinner">
<i class="fa fa-database"></i>
<span class="hidden-sm">{% trans "Storage" %}</span>
<i class="fa fa-spinner" style="display:none; margin-left:4px;"></i>
</a>
</li>
<li>
......
......@@ -13,7 +13,11 @@
<div class="col-md-5">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin"><i class="fa fa-database"></i> {% trans "Datastore" %}</h3>
<h3 class="no-margin">
<i class="fa fa-database"></i>i
{% trans "Datastore" %}
<i id="ds-spinner" class="fa fa-spinner" style="display:none;"></i>
</h3>
</div>
<div class="panel-body">
<form method="get" action="">
......@@ -111,7 +115,9 @@
{% if stats %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin"><i class="fa fa-file"></i> {% trans "Disks" %}</h3>
<h3 class="no-margin"><i class="fa fa-file"></i> {% trans "Disks" %}
<i id="disks-spinner" class="fa fa-spinner" style="display:none;"></i>
</h3>
</div>
<div class="panel-body">
<div class="row">
......@@ -119,11 +125,13 @@
<ul class="nav nav-pills" style="margin: 5px 0 20px 0;">
<li class="disabled"><a href="#">{% trans "Filter by type" %}</a></li>
<li {% if not request.GET.filter %} class="active"{% endif %}>
<a href="{{ request.path }}?s={{ request.GET.s }}">{% trans "ALL" %}</a>
<a href="{{ request.path }}?ds={{ ds_selected }}&s={{ request.GET.s }}" class="storage-filter">
{% trans "ALL" %}
</a>
</li>
{% for f in filter_names %}
<li{% if request.GET.filter == f.0 %} class="active"{% endif %}>
<a href="?filter={{ f.0 }}&s={{ request.GET.s }}">{{ f.1|capfirst }}</a>
<a href="?ds={{ ds_selected }}&filter={{ f.0 }}&s={{ request.GET.s }}"class="storage-filter">{{ f.1|capfirst }}</a>
</li>
{% endfor %}
</ul>
......@@ -134,6 +142,7 @@
<input type="text" name="s" class="form-control"
value="{{ request.GET.s }}" placeholder="{% trans "Search..." %}"/>
<input type="hidden" name="filter" value="{{ request.GET.filter }}"/>
<input type="hidden" name="ds" value="{{ ds_selected }}"/>
<span class="input-group-btn">
<button class="btn btn-primary"><i class="fa fa-search"></i></button>
</span>
......
......@@ -17,6 +17,7 @@
from __future__ import unicode_literals, absolute_import
import errno
import logging
from django.contrib import messages
from django.core.urlresolvers import reverse
......@@ -35,6 +36,8 @@ from ..forms import DataStoreForm, DiskForm
from django.shortcuts import get_object_or_404, redirect
from django.db import IntegrityError
logger = logging.getLogger(__name__)
class StorageDetail(SuperuserRequiredMixin, TemplateView):
template_name = "dashboard/storage/detail.html"
......@@ -147,7 +150,7 @@ class StorageDetail(SuperuserRequiredMixin, TemplateView):
messages.success(request, _("Datastore updated."))
return self._redirect_with_ds(ds.pk)
def get_table_data(self, ds):
def get_table_data(self, ds, missing):
if ds is None:
return Disk.objects.none()
......@@ -163,12 +166,18 @@ class StorageDetail(SuperuserRequiredMixin, TemplateView):
}
if filter_name:
qs = qs.filter(**filter_queries.get(filter_name, {}))
if filter_name == 'missing':
qs = missing
else:
qs = qs.filter(**filter_queries.get(filter_name, {}))
if search:
search = search.strip()
qs = qs.filter(Q(name__icontains=search) |
Q(filename__icontains=search))
qs = qs.filter(
Q(name__icontains=search) |
Q(filename__icontains=search) |
Q(instance_set__name__icontains=search) |
Q(template_set__name__icontains=search)
).distinct()
return qs
......@@ -235,7 +244,8 @@ class StorageDetail(SuperuserRequiredMixin, TemplateView):
context["orphan_disks"] = None
context["disk_table"] = DiskListTable(
self.get_table_data(ds), request=self.request,
self.get_table_data(ds, missing = context["missing_disks"]),
request=self.request,
template="django_tables2/with_pagination.html"
)
......@@ -243,15 +253,27 @@ class StorageDetail(SuperuserRequiredMixin, TemplateView):
('vm', _("virtual machine")),
('template', _("template")),
('none', _("none")),
('missing', _("missing")),
)
return context
class DiskDetail(SuperuserRequiredMixin, UpdateView):
model = Disk
form_class = DiskForm
template_name = "dashboard/storage/disk.html"
def form_valid(self, form):
pass
# Save only allowed edits (datastore) and redirect back.
self.object = form.save()
messages.success(self.request, _("Disk updated."))
return redirect(self.request.path)
#class DiskDetail(SuperuserRequiredMixin, UpdateView):
# model = Disk
# form_class = DiskForm
# template_name = "dashboard/storage/disk.html"
#
# def form_valid(self, form):
# pass
#
......@@ -62,10 +62,10 @@ def common_select(instance, nodes):
nodes = [n for n in nodes
if n.schedule_enabled and n.online and
has_traits(instance.req_traits.all(), n)]
logger.error('capab: %s 0selected_nodes: %s', instance.capability_group, nodes)
logger.debug('capab: %s selected_nodes: %s', instance.capability_group, nodes)
if instance.capability_group:
nodes = [n for n in nodes if n.capability == instance.capability_group]
logger.error('capab: %s selected_nodes: %s', instance.capability_group, nodes)
nodes = [n for n in nodes if not n.capability or n.capability == instance.capability_group]
logger.debug('capab: %s selected_nodes: %s', instance.capability_group, nodes)
if not nodes:
logger.warning('select_node: no usable node for %s', unicode(instance))
raise TraitsUnsatisfiableException()
......
......@@ -39,16 +39,17 @@ def garbage_collector(timeout=15):
args=[ds.path], queue=queue_name).get(timeout=timeout))
disks = set(ds.get_deletable_disks())
queue_name = ds.get_remote_queue_name('storage', priority='slow')
for i in disks & files:
logger.info("Image: %s at Datastore: %s moved to trash folder." %
(i, ds.path))
storage_tasks.move_to_trash.apply_async(
args=[ds.path, i], queue=queue_name).get(timeout=timeout)
try:
for i in disks & files:
logger.error("Image: %s at Datastore: %s moved to trash folder." %
(i, ds.path))
storage_tasks.move_to_trash.apply_async(
args=[ds.path, i], queue=queue_name).get(timeout=timeout)
storage_tasks.make_free_space.apply_async(
args=[ds.path], queue=queue_name).get(timeout=timeout)
except Exception as e:
logger.warning(str(e))
logger.error(str(e))
@celery.task
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment