Commit df7e52cf by Kálmán Viktor

Merge branch 'master' into feature-new-tour

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/views.py
parents ad5e4e06 23000834
...@@ -39,3 +39,4 @@ circle/static_collected ...@@ -39,3 +39,4 @@ circle/static_collected
# jsi18n files # jsi18n files
jsi18n jsi18n
scripts.rc
...@@ -26,6 +26,17 @@ from .models import activity_context, has_suffix, humanize_exception ...@@ -26,6 +26,17 @@ from .models import activity_context, has_suffix, humanize_exception
logger = getLogger(__name__) logger = getLogger(__name__)
class SubOperationMixin(object):
required_perms = ()
def create_activity(self, parent, user, kwargs):
if not parent:
raise TypeError("SubOperation can only be called with "
"parent_activity specified.")
return super(SubOperationMixin, self).create_activity(
parent, user, kwargs)
class Operation(object): class Operation(object):
"""Base class for VM operations. """Base class for VM operations.
""" """
...@@ -36,6 +47,10 @@ class Operation(object): ...@@ -36,6 +47,10 @@ class Operation(object):
abortable = False abortable = False
has_percentage = False has_percentage = False
@classmethod
def get_activity_code_suffix(cls):
return cls.id
def __call__(self, **kwargs): def __call__(self, **kwargs):
return self.call(**kwargs) return self.call(**kwargs)
...@@ -232,7 +247,7 @@ class OperatedMixin(object): ...@@ -232,7 +247,7 @@ class OperatedMixin(object):
operation could be found. operation could be found.
""" """
for op in getattr(self, operation_registry_name, {}).itervalues(): for op in getattr(self, operation_registry_name, {}).itervalues():
if has_suffix(activity_code, op.activity_code_suffix): if has_suffix(activity_code, op.get_activity_code_suffix()):
return op(self) return op(self)
else: else:
return None return None
...@@ -273,3 +288,4 @@ def register_operation(op_cls, op_id=None, target_cls=None): ...@@ -273,3 +288,4 @@ def register_operation(op_cls, op_id=None, target_cls=None):
setattr(target_cls, operation_registry_name, dict()) setattr(target_cls, operation_registry_name, dict())
getattr(target_cls, operation_registry_name)[op_id] = op_cls getattr(target_cls, operation_registry_name)[op_id] = op_cls
return op_cls
...@@ -27,9 +27,7 @@ class OperationTestCase(TestCase): ...@@ -27,9 +27,7 @@ class OperationTestCase(TestCase):
class AbortEx(Exception): class AbortEx(Exception):
pass pass
op = Operation(MagicMock()) op = TestOp(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
op.async_operation = MagicMock( op.async_operation = MagicMock(
apply_async=MagicMock(side_effect=AbortEx)) apply_async=MagicMock(side_effect=AbortEx))
...@@ -44,9 +42,7 @@ class OperationTestCase(TestCase): ...@@ -44,9 +42,7 @@ class OperationTestCase(TestCase):
class AbortEx(Exception): class AbortEx(Exception):
pass pass
op = Operation(MagicMock()) op = TestOp(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
with patch.object(Operation, 'create_activity', side_effect=AbortEx): with patch.object(Operation, 'create_activity', side_effect=AbortEx):
with patch.object(Operation, 'check_precond') as chk_pre: with patch.object(Operation, 'check_precond') as chk_pre:
try: try:
...@@ -55,9 +51,7 @@ class OperationTestCase(TestCase): ...@@ -55,9 +51,7 @@ class OperationTestCase(TestCase):
self.assertTrue(chk_pre.called) self.assertTrue(chk_pre.called)
def test_auth_check_on_non_system_call(self): def test_auth_check_on_non_system_call(self):
op = Operation(MagicMock()) op = TestOp(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
user = MagicMock() user = MagicMock()
with patch.object(Operation, 'check_auth') as check_auth: with patch.object(Operation, 'check_auth') as check_auth:
with patch.object(Operation, 'check_precond'), \ with patch.object(Operation, 'check_precond'), \
...@@ -67,9 +61,7 @@ class OperationTestCase(TestCase): ...@@ -67,9 +61,7 @@ class OperationTestCase(TestCase):
check_auth.assert_called_with(user) check_auth.assert_called_with(user)
def test_no_auth_check_on_system_call(self): def test_no_auth_check_on_system_call(self):
op = Operation(MagicMock()) op = TestOp(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
with patch.object(Operation, 'check_auth', side_effect=AssertionError): with patch.object(Operation, 'check_auth', side_effect=AssertionError):
with patch.object(Operation, 'check_precond'), \ with patch.object(Operation, 'check_precond'), \
patch.object(Operation, 'create_activity'), \ patch.object(Operation, 'create_activity'), \
...@@ -77,39 +69,25 @@ class OperationTestCase(TestCase): ...@@ -77,39 +69,25 @@ class OperationTestCase(TestCase):
op.call(system=True) op.call(system=True)
def test_no_exception_for_more_arguments_when_operation_takes_kwargs(self): def test_no_exception_for_more_arguments_when_operation_takes_kwargs(self):
class KwargOp(Operation): op = TestOp(MagicMock())
activity_code_suffix = 'test' with patch.object(TestOp, 'create_activity'), \
id = 'test' patch.object(TestOp, '_exec_op'):
def _operation(self, **kwargs):
pass
op = KwargOp(MagicMock())
with patch.object(KwargOp, 'create_activity'), \
patch.object(KwargOp, '_exec_op'):
op.call(system=True, foo=42) op.call(system=True, foo=42)
def test_exception_for_unexpected_arguments(self): def test_exception_for_unexpected_arguments(self):
class TestOp(Operation):
activity_code_suffix = 'test'
id = 'test'
def _operation(self):
pass
op = TestOp(MagicMock()) op = TestOp(MagicMock())
with patch.object(TestOp, 'create_activity'), \ with patch.object(TestOp, 'create_activity'), \
patch.object(TestOp, '_exec_op'): patch.object(TestOp, '_exec_op'):
self.assertRaises(TypeError, op.call, system=True, foo=42) self.assertRaises(TypeError, op.call, system=True, bar=42)
def test_exception_for_missing_arguments(self): def test_exception_for_missing_arguments(self):
class TestOp(Operation):
activity_code_suffix = 'test'
id = 'test'
def _operation(self, foo):
pass
op = TestOp(MagicMock()) op = TestOp(MagicMock())
with patch.object(TestOp, 'create_activity'): with patch.object(TestOp, 'create_activity'):
self.assertRaises(TypeError, op.call, system=True) self.assertRaises(TypeError, op.call, system=True)
class TestOp(Operation):
id = 'test'
def _operation(self, foo):
pass
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
from __future__ import absolute_import from __future__ import absolute_import
from datetime import timedelta from datetime import timedelta
from urlparse import urlparse
from django.contrib.auth.forms import ( from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm, AuthenticationForm, PasswordResetForm, SetPasswordForm,
...@@ -39,6 +40,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm ...@@ -39,6 +40,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.forms.widgets import TextInput, HiddenInput from django.forms.widgets import TextInput, HiddenInput
from django.template import Context from django.template import Context
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from sizefield.widgets import FileSizeWidget from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
...@@ -79,6 +81,12 @@ class VmSaveForm(forms.Form): ...@@ -79,6 +81,12 @@ class VmSaveForm(forms.Form):
helper.form_tag = False helper.form_tag = False
return helper return helper
def __init__(self, *args, **kwargs):
default = kwargs.pop('default', None)
super(VmSaveForm, self).__init__(*args, **kwargs)
if default:
self.fields['name'].initial = default
class VmCustomizeForm(forms.Form): class VmCustomizeForm(forms.Form):
name = forms.CharField(widget=forms.TextInput(attrs={ name = forms.CharField(widget=forms.TextInput(attrs={
...@@ -524,11 +532,7 @@ class TemplateForm(forms.ModelForm): ...@@ -524,11 +532,7 @@ class TemplateForm(forms.ModelForm):
value = field.widget.value_from_datadict( value = field.widget.value_from_datadict(
self.data, self.files, self.add_prefix(name)) self.data, self.files, self.add_prefix(name))
try: try:
if isinstance(field, forms.FileField): value = field.clean(value)
initial = self.initial.get(name, field.initial)
value = field.clean(value, initial)
else:
value = field.clean(value)
self.cleaned_data[name] = value self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name): if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)() value = getattr(self, 'clean_%s' % name)()
...@@ -544,13 +548,14 @@ class TemplateForm(forms.ModelForm): ...@@ -544,13 +548,14 @@ class TemplateForm(forms.ModelForm):
else: else:
self.cleaned_data[name] = getattr(old, name) self.cleaned_data[name] = getattr(old, name)
if "req_traits" not in self.allowed_fields:
self.cleaned_data['req_traits'] = self.instance.req_traits.all()
def save(self, commit=True): def save(self, commit=True):
data = self.cleaned_data data = self.cleaned_data
self.instance.max_ram_size = data.get('ram_size') self.instance.max_ram_size = data.get('ram_size')
instance = super(TemplateForm, self).save(commit=False) instance = super(TemplateForm, self).save(commit=True)
if commit:
instance.save()
# create and/or delete InterfaceTemplates # create and/or delete InterfaceTemplates
networks = InterfaceTemplate.objects.filter( networks = InterfaceTemplate.objects.filter(
...@@ -755,6 +760,7 @@ class VmStateChangeForm(forms.Form): ...@@ -755,6 +760,7 @@ class VmStateChangeForm(forms.Form):
"but don't interrupt any tasks.")) "but don't interrupt any tasks."))
new_state = forms.ChoiceField(Instance.STATUS, label=_( new_state = forms.ChoiceField(Instance.STATUS, label=_(
"New status")) "New status"))
reset_node = forms.BooleanField(required=False, label=_("Reset node"))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
show_interrupt = kwargs.pop('show_interrupt') show_interrupt = kwargs.pop('show_interrupt')
...@@ -772,6 +778,17 @@ class VmStateChangeForm(forms.Form): ...@@ -772,6 +778,17 @@ class VmStateChangeForm(forms.Form):
return helper return helper
class RedeployForm(forms.Form):
with_emergency_change_state = forms.BooleanField(
required=False, initial=True, label=_("use emergency state change"))
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class VmCreateDiskForm(forms.Form): class VmCreateDiskForm(forms.Form):
name = forms.CharField(max_length=100, label=_("Name")) name = forms.CharField(max_length=100, label=_("Name"))
size = forms.CharField( size = forms.CharField(
...@@ -779,6 +796,12 @@ class VmCreateDiskForm(forms.Form): ...@@ -779,6 +796,12 @@ class VmCreateDiskForm(forms.Form):
help_text=_('Size of disk to create in bytes or with units ' help_text=_('Size of disk to create in bytes or with units '
'like MB or GB.')) 'like MB or GB.'))
def __init__(self, *args, **kwargs):
default = kwargs.pop('default', None)
super(VmCreateDiskForm, self).__init__(*args, **kwargs)
if default:
self.fields['name'].initial = default
def clean_size(self): def clean_size(self):
size_in_bytes = self.cleaned_data.get("size") size_in_bytes = self.cleaned_data.get("size")
if not size_in_bytes.isdigit() and len(size_in_bytes) > 0: if not size_in_bytes.isdigit() and len(size_in_bytes) > 0:
...@@ -793,8 +816,79 @@ class VmCreateDiskForm(forms.Form): ...@@ -793,8 +816,79 @@ class VmCreateDiskForm(forms.Form):
return helper return helper
class VmDiskResizeForm(forms.Form):
size = forms.CharField(
widget=FileSizeWidget, initial=(10 << 30), label=_('Size'),
help_text=_('Size to resize the disk in bytes or with units '
'like MB or GB.'))
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.disk = kwargs.pop('default')
super(VmDiskResizeForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'disk', forms.ModelChoiceField(
queryset=choices, initial=self.disk, required=True,
empty_label=None, label=_('Disk')))
if self.disk:
self.fields['disk'].widget = HiddenInput()
self.fields['size'].initial += self.disk.size
def clean(self):
cleaned_data = super(VmDiskResizeForm, self).clean()
size_in_bytes = self.cleaned_data.get("size")
disk = self.cleaned_data.get('disk')
if not size_in_bytes.isdigit() and len(size_in_bytes) > 0:
raise forms.ValidationError(_("Invalid format, you can use "
" GB or MB!"))
if int(size_in_bytes) < int(disk.size):
raise forms.ValidationError(_("Disk size must be greater than the "
"actual size."))
return cleaned_data
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
if self.disk:
helper.layout = Layout(
HTML(_("<label>Disk:</label> %s") % escape(self.disk)),
Field('disk'), Field('size'))
return helper
class VmDiskRemoveForm(forms.Form):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.disk = kwargs.pop('default')
super(VmDiskRemoveForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'disk', forms.ModelChoiceField(
queryset=choices, initial=self.disk, required=True,
empty_label=None, label=_('Disk')))
if self.disk:
self.fields['disk'].widget = HiddenInput()
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
if self.disk:
helper.layout = Layout(
AnyTag(
"div",
HTML(_("<label>Disk:</label> %s") % escape(self.disk)),
css_class="form-group",
),
Field("disk"),
)
return helper
class VmDownloadDiskForm(forms.Form): class VmDownloadDiskForm(forms.Form):
name = forms.CharField(max_length=100, label=_("Name")) name = forms.CharField(max_length=100, label=_("Name"), required=False)
url = forms.CharField(label=_('URL'), validators=[URLValidator(), ]) url = forms.CharField(label=_('URL'), validators=[URLValidator(), ])
@property @property
...@@ -803,6 +897,18 @@ class VmDownloadDiskForm(forms.Form): ...@@ -803,6 +897,18 @@ class VmDownloadDiskForm(forms.Form):
helper.form_tag = False helper.form_tag = False
return helper return helper
def clean(self):
cleaned_data = super(VmDownloadDiskForm, self).clean()
if not cleaned_data['name']:
if cleaned_data['url']:
cleaned_data['name'] = urlparse(
cleaned_data['url']).path.split('/')[-1]
if not cleaned_data['name']:
raise forms.ValidationError(
_("Could not find filename in URL, "
"please specify a name explicitly."))
return cleaned_data
class VmAddInterfaceForm(forms.Form): class VmAddInterfaceForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
......
...@@ -528,7 +528,7 @@ footer a, footer a:hover, footer a:visited { ...@@ -528,7 +528,7 @@ footer a, footer a:hover, footer a:visited {
} }
#dashboard-template-list a small { #dashboard-template-list a small {
max-width: 50%; max-width: 45%;
float: left; float: left;
padding-top: 2px; padding-top: 2px;
text-overflow: ellipsis; text-overflow: ellipsis;
...@@ -991,3 +991,42 @@ textarea[name="new_members"] { ...@@ -991,3 +991,42 @@ textarea[name="new_members"] {
.introjs-button:hover { .introjs-button:hover {
color: #428bca; color: #428bca;
} }
#vm-info-pane {
margin-bottom: 20px;
}
.node-list-table tbody>tr>td, .node-list-table thead>tr>th {
vertical-align: middle;
}
.node-list-table thead>tr>th,
.node-list-table .enabled, .node-list-table .priority,
.node-list-table .overcommit, .node-list-table .number_of_VMs {
text-align: center;
}
.node-list-table-thin {
width: 10px;
}
.node-list-table-monitor {
width: 250px;
}
.graph-images img {
max-width: 100%;
}
#vm-list-table td.state,
#vm-list-table td.memory {
white-space: nowrap;
}
#vm-list-table td {
vertical-align: middle;
}
.disk-resize-btn {
margin-right: 5px;
}
...@@ -234,6 +234,7 @@ $(function () { ...@@ -234,6 +234,7 @@ $(function () {
'host': result[i].host, 'host': result[i].host,
'icon': result[i].icon, 'icon': result[i].icon,
'status': result[i].status, 'status': result[i].status,
'owner': result[i].owner,
}); });
} }
}); });
...@@ -251,7 +252,7 @@ $(function () { ...@@ -251,7 +252,7 @@ $(function () {
search_result.sort(compareVmByFav); search_result.sort(compareVmByFav);
for(var i=0; i<5 && i<search_result.length; i++) for(var i=0; i<5 && i<search_result.length; i++)
html += generateVmHTML(search_result[i].pk, search_result[i].name, html += generateVmHTML(search_result[i].pk, search_result[i].name,
search_result[i].host, search_result[i].icon, search_result[i].owner ? search_result[i].owner : search_result[i].host, search_result[i].icon,
search_result[i].status, search_result[i].fav, search_result[i].status, search_result[i].fav,
(search_result.length < 5)); (search_result.length < 5));
if(search_result.length == 0) if(search_result.length == 0)
...@@ -396,6 +397,20 @@ $(function () { ...@@ -396,6 +397,20 @@ $(function () {
clientInstalledAction(connectUri); clientInstalledAction(connectUri);
return false; return false;
}); });
/* change graphs */
$(".graph-buttons a").click(function() {
var time = $(this).data("graph-time");
$(".graph-images img").each(function() {
var src = $(this).prop("src");
var new_src = src.substring(0, src.lastIndexOf("/") + 1) + time;
$(this).prop("src", new_src);
});
// change the buttons too
$(".graph-buttons a").removeClass("btn-primary").addClass("btn-default");
$(this).removeClass("btn-default").addClass("btn-primary");
return false;
});
}); });
function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { function generateVmHTML(pk, name, host, icon, _status, fav, is_last) {
...@@ -603,7 +618,7 @@ function addModalConfirmation(func, data) { ...@@ -603,7 +618,7 @@ function addModalConfirmation(func, data) {
} }
function clientInstalledAction(location) { function clientInstalledAction(location) {
setCookie('downloaded_client', true, 365 * 24 * 60 * 60, "/"); setCookie('downloaded_client', true, 365 * 24 * 60 * 60 * 1000, "/");
window.location.href = location; window.location.href = location;
$('#confirmation-modal').modal("hide"); $('#confirmation-modal').modal("hide");
} }
......
...@@ -51,7 +51,8 @@ $(function() { ...@@ -51,7 +51,8 @@ $(function() {
if(data.success) { if(data.success) {
$('a[href="#activity"]').trigger("click"); $('a[href="#activity"]').trigger("click");
if(data.with_reload) { if(data.with_reload) {
location.reload(); // when the activity check stops the page will reload
reload_vm_detail = true;
} }
/* if there are messages display them */ /* if there are messages display them */
......
var show_all = false; var show_all = false;
var in_progress = false; var in_progress = false;
var activity_hash = 5; var activity_hash = 5;
var reload_vm_detail = false;
$(function() { $(function() {
/* do we need to check for new activities */ /* do we need to check for new activities */
...@@ -419,6 +420,7 @@ function checkNewActivity(runs) { ...@@ -419,6 +420,7 @@ function checkNewActivity(runs) {
); );
} else { } else {
in_progress = false; in_progress = false;
if(reload_vm_detail) location.reload();
} }
$('a[href="#activity"] i').removeClass('fa-spin'); $('a[href="#activity"] i').removeClass('fa-spin');
}, },
......
...@@ -19,8 +19,7 @@ from __future__ import absolute_import ...@@ -19,8 +19,7 @@ from __future__ import absolute_import
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django_tables2 import Table, A from django_tables2 import Table, A
from django_tables2.columns import (TemplateColumn, Column, BooleanColumn, from django_tables2.columns import TemplateColumn, Column, LinkColumn
LinkColumn)
from vm.models import Node, InstanceTemplate, Lease from vm.models import Node, InstanceTemplate, Lease
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
...@@ -40,8 +39,10 @@ class NodeListTable(Table): ...@@ -40,8 +39,10 @@ class NodeListTable(Table):
attrs={'th': {'class': 'node-list-table-thin'}}, attrs={'th': {'class': 'node-list-table-thin'}},
) )
enabled = BooleanColumn( get_status_display = Column(
verbose_name=_("Status"),
attrs={'th': {'class': 'node-list-table-thin'}}, attrs={'th': {'class': 'node-list-table-thin'}},
order_by=("enabled", "schedule_enabled"),
) )
name = TemplateColumn( name = TemplateColumn(
...@@ -66,20 +67,12 @@ class NodeListTable(Table): ...@@ -66,20 +67,12 @@ class NodeListTable(Table):
orderable=False, orderable=False,
) )
actions = TemplateColumn(
verbose_name=_("Actions"),
attrs={'th': {'class': 'node-list-table-thin'}},
template_code=('{% include "dashboard/node-list/column-'
'actions.html" with btn_size="btn-xs" %}'),
orderable=False,
)
class Meta: class Meta:
model = Node model = Node
attrs = {'class': ('table table-bordered table-striped table-hover ' attrs = {'class': ('table table-bordered table-striped table-hover '
'node-list-table')} 'node-list-table')}
fields = ('pk', 'name', 'host', 'enabled', 'priority', 'overcommit', fields = ('pk', 'name', 'host', 'get_status_display', 'priority',
'number_of_VMs', ) 'overcommit', 'number_of_VMs', )
class GroupListTable(Table): class GroupListTable(Table):
......
<img src="{{ STATIC_URL}}dashboard/img/logo.png" style="height: 25px;"/>
<img src="{{ STATIC_URL}}local-logo.png" style="padding-left: 2px; height: 25px;"/>
{% load i18n %} {% load i18n %}
{% load sizefieldtags %} {% load sizefieldtags %}
<i class="fa {% if d.is_downloading %}fa-refresh fa-spin{% else %}fa-file{% if d.failed %}" style="color: #d9534f;{% endif %}{% endif %}"></i> <i class="fa fa-file"></i>
{{ d.name }} (#{{ d.id }}) - {{ d.name }} (#{{ d.id }})
{% if not d.is_downloading %}
{% if not d.failed %} {% if op.remove_disk %}
{% if d.size %}{{ d.size|filesize }}{% endif %} <span class="operation-wrapper">
{% else %} <a href="{{ op.remove_disk.get_url }}?disk={{d.pk}}"
<div class="label label-danger"{% if user.is_superuser %} title="{{ d.get_latest_activity_result }}"{% endif %}>{% trans "failed" %}</div> class="btn btn-xs btn-{{ op.remove_disk.effect}} pull-right operation disk-remove-btn
{% endif %} {% if op.resize_disk.disabled %}disabled{% endif %}">
{% else %}<span class="disk-list-disk-percentage" data-disk-pk="{{ d.pk }}">{{ d.get_download_percentage }}</span>%{% endif %} <i class="fa fa-{{ op.remove_disk.icon }}"></i> {% trans "Remove" %}
{% if is_owner != False %} </a>
<a href="{% url "dashboard.views.disk-remove" pk=d.pk %}?next={{ request.path }}" </span>
data-disk-pk="{{ d.pk }}" class="btn btn-xs btn-danger pull-right disk-remove" {% endif %}
{% if not long_remove %}title="{% trans "Remove" %}"{% endif %} {% if op.resize_disk %}
> <span class="operation-wrapper">
<i class="fa fa-times"></i>{% if long_remove %} {% trans "Remove" %}{% endif %} <a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}"
</a> class="btn btn-xs btn-{{ op.resize_disk.effect }} pull-right operation disk-resize-btn
{% if op.resize_disk.disabled %}disabled{% endif %}">
<i class="fa fa-{{ op.resize_disk.icon }}"></i> {% trans "Resize" %}
</a>
</span>
{% endif %} {% endif %}
<div style="clear: both;"></div> <div style="clear: both;"></div>
{% for o in graph_time_options %}
<a class="btn btn-xs
btn-{% if graph_time == o.time %}primary{% else %}default{% endif %}"
href="?graph_time={{ o.time }}"
data-graph-time="{{ o.time }}">
{{ o.name }}
</a>
{% endfor %}
...@@ -13,17 +13,19 @@ Choose a compute node to migrate {{obj}} to. ...@@ -13,17 +13,19 @@ Choose a compute node to migrate {{obj}} to.
{% block formfields %} {% block formfields %}
<ul id="vm-migrate-node-list" class="list-unstyled"> <ul id="vm-migrate-node-list" class="list-unstyled">
{% with current=object.node.pk selected=object.select_node.pk %} {% with current=object.node.pk %}