diff --git a/docs/source/index.rst b/docs/source/index.rst index e8946c26e..ae0d6f368 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -84,6 +84,7 @@ In-depth documentation for Horizon and it's APIs. ref/run_tests ref/horizon ref/tables + ref/tabs ref/users ref/forms ref/views diff --git a/docs/source/ref/tabs.rst b/docs/source/ref/tabs.rst new file mode 100644 index 000000000..9da824ab2 --- /dev/null +++ b/docs/source/ref/tabs.rst @@ -0,0 +1,37 @@ +========================== +Horizon Tabs and TabGroups +========================== + +.. module:: horizon.tabs + +Horizon includes a set of reusable components for programmatically +building tabbed interfaces with fancy features like dynamic AJAX loading +and nearly effortless templating and styling. + +Tab Groups +========== + +For any tabbed interface, your fundamental element is the tab group which +contains all your tabs. This class provides a dead-simple API for building +tab groups and encapsulates all the necessary logic behind the scenes. + +.. autoclass:: TabGroup + :members: + +Tabs +==== + +The tab itself is the discrete unit for a tab group, representing one +view of data. + +.. autoclass:: Tab + :members: + +TabView +======= + +There is also a useful and simple generic class-based view for handling +the display of a :class:`~horizon.tabs.TabGroup` class. + +.. autoclass:: TabView + :members: diff --git a/horizon/horizon/dashboards/nova/instances_and_volumes/instances/tabs.py b/horizon/horizon/dashboards/nova/instances_and_volumes/instances/tabs.py new file mode 100644 index 000000000..ea114c7ea --- /dev/null +++ b/horizon/horizon/dashboards/nova/instances_and_volumes/instances/tabs.py @@ -0,0 +1,74 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 Nebula, Inc. +# +# 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. + +from django.utils.translation import ugettext as _ + +from horizon import api +from horizon import exceptions +from horizon import tabs + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = ("nova/instances_and_volumes/instances/" + "_detail_overview.html") + + def get_context_data(self, request): + return {"instance": self.tab_group.kwargs['instance']} + + +class LogTab(tabs.Tab): + name = _("Log") + slug = "log" + template_name = "nova/instances_and_volumes/instances/_detail_log.html" + preload = False + + def get_context_data(self, request): + instance = self.tab_group.kwargs['instance'] + try: + data = api.server_console_output(request, instance.id) + except: + data = _('Unable to get log for instance "%s".') % instance.id + exceptions.handle(request, ignore=True) + return {"instance": instance, + "console_log": data} + + +class VNCTab(tabs.Tab): + name = _("VNC") + slug = "vnc" + template_name = "nova/instances_and_volumes/instances/_detail_vnc.html" + preload = False + + def get_context_data(self, request): + instance = self.tab_group.kwargs['instance'] + try: + console = api.nova.server_vnc_console(request, instance.id) + vnc_url = "%s&title=%s(%s)" % (console.url, + getattr(instance, "name", ""), + instance.id) + except: + vnc_url = None + exceptions.handle(request, + _('Unable to get vnc console for ' + 'instance "%s".') % instance.id) + return {'vnc_url': vnc_url} + + +class InstanceDetailTabs(tabs.TabGroup): + slug = "instance_details" + tabs = (OverviewTab, LogTab, VNCTab) diff --git a/horizon/horizon/dashboards/nova/instances_and_volumes/instances/tests.py b/horizon/horizon/dashboards/nova/instances_and_volumes/instances/tests.py index 3810c64ad..6bb8d7025 100644 --- a/horizon/horizon/dashboards/nova/instances_and_volumes/instances/tests.py +++ b/horizon/horizon/dashboards/nova/instances_and_volumes/instances/tests.py @@ -25,6 +25,7 @@ from novaclient import exceptions as nova_exceptions from horizon import api from horizon import test +from .tabs import InstanceDetailTabs INDEX_URL = reverse('horizon:nova:instances_and_volumes:index') @@ -199,36 +200,38 @@ class InstanceViewTests(test.TestCase): res = self.client.post(INDEX_URL, formData) self.assertRedirectsNoFollow(res, INDEX_URL) - def test_instance_console(self): + def test_instance_log(self): server = self.servers.first() CONSOLE_OUTPUT = 'output' self.mox.StubOutWithMock(api, 'server_console_output') api.server_console_output(IsA(http.HttpRequest), - server.id, - tail_length=None).AndReturn(CONSOLE_OUTPUT) + server.id).AndReturn(CONSOLE_OUTPUT) self.mox.ReplayAll() url = reverse('horizon:nova:instances_and_volumes:instances:console', args=[server.id]) - res = self.client.get(url) + tg = InstanceDetailTabs(self.request) + qs = "?%s=%s" % (tg.param_name, tg.get_tab("log").get_id()) + res = self.client.get(url + qs) self.assertIsInstance(res, http.HttpResponse) self.assertContains(res, CONSOLE_OUTPUT) - def test_instance_console_exception(self): + def test_instance_log_exception(self): server = self.servers.first() self.mox.StubOutWithMock(api, 'server_console_output') exc = nova_exceptions.ClientException(500) api.server_console_output(IsA(http.HttpRequest), - server.id, - tail_length=None).AndRaise(exc) + server.id).AndRaise(exc) self.mox.ReplayAll() url = reverse('horizon:nova:instances_and_volumes:instances:console', args=[server.id]) - res = self.client.get(url) - self.assertRedirectsNoFollow(res, INDEX_URL) + tg = InstanceDetailTabs(self.request) + qs = "?%s=%s" % (tg.param_name, tg.get_tab("log").get_id()) + res = self.client.get(url + qs) + self.assertContains(res, "Unable to get log for") def test_instance_vnc(self): server = self.servers.first() diff --git a/horizon/horizon/dashboards/nova/instances_and_volumes/instances/views.py b/horizon/horizon/dashboards/nova/instances_and_volumes/instances/views.py index b53b514fb..001e98e63 100644 --- a/horizon/horizon/dashboards/nova/instances_and_volumes/instances/views.py +++ b/horizon/horizon/dashboards/nova/instances_and_volumes/instances/views.py @@ -32,8 +32,9 @@ from django.utils.translation import ugettext as _ from horizon import api from horizon import exceptions from horizon import forms -from horizon import views +from horizon import tabs from .forms import UpdateInstance +from .tabs import InstanceDetailTabs LOG = logging.getLogger(__name__) @@ -42,18 +43,14 @@ LOG = logging.getLogger(__name__) def console(request, instance_id): try: # TODO(jakedahn): clean this up once the api supports tailing. - length = request.GET.get('length', None) - console = api.server_console_output(request, - instance_id, - tail_length=length) - response = http.HttpResponse(mimetype='text/plain') - response.write(console) - response.flush() - return response + data = api.server_console_output(request, instance_id) except: - msg = _('Unable to get log for instance "%s".') % instance_id - redirect = reverse('horizon:nova:instances_and_volumes:index') - exceptions.handle(request, msg, redirect=redirect) + data = _('Unable to get log for instance "%s".') % instance_id + exceptions.handle(request, ignore=True) + response = http.HttpResponse(mimetype='text/plain') + response.write(data) + response.flush() + return response def vnc(request, instance_id): @@ -90,47 +87,37 @@ class UpdateView(forms.ModalFormView): 'name': getattr(self.object, 'name', '')} -class DetailView(views.APIView): +class DetailView(tabs.TabView): + tab_group_class = InstanceDetailTabs template_name = 'nova/instances_and_volumes/instances/detail.html' - def get_data(self, request, context, *args, **kwargs): - instance_id = kwargs['instance_id'] - - if "show" in request.GET: - show_tab = request.GET["show"] - else: - show_tab = "overview" - - try: - instance = api.server_get(request, instance_id) - volumes = api.volume_instance_list(request, instance_id) - - # Gather our flavors and images and correlate our instances to - # them. Exception handling happens in the parent class. - flavors = api.flavor_list(request) - full_flavors = SortedDict([(str(flavor.id), flavor) for \ - flavor in flavors]) - instance.full_flavor = full_flavors[instance.flavor["id"]] - - context.update({'instance': instance, 'volumes': volumes}) - except: - redirect = reverse('horizon:nova:instances_and_volumes:index') - exceptions.handle(request, - _('Unable to retrieve details for ' - 'instance "%s".') % instance_id, - redirect=redirect) - if show_tab == "vnc": - try: - console = api.server_vnc_console(request, instance_id) - vnc_url = "%s&title=%s(%s)" % (console.url, - getattr(instance, "name", ""), - instance_id) - context.update({'vnc_url': vnc_url}) - except: - exceptions.handle(request, - _('Unable to get vnc console for ' - 'instance "%s".') % instance_id) - - context.update({'show_tab': show_tab}) - + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + context["instance"] = self.get_data() return context + + def get_data(self): + if not hasattr(self, "_instance"): + try: + instance_id = self.kwargs['instance_id'] + instance = api.server_get(self.request, instance_id) + instance.volumes = api.volume_instance_list(self.request, + instance_id) + # Gather our flavors and images and correlate our instances to + # them. Exception handling happens in the parent class. + flavors = api.flavor_list(self.request) + full_flavors = SortedDict([(str(flavor.id), flavor) for \ + flavor in flavors]) + instance.full_flavor = full_flavors[instance.flavor["id"]] + except: + redirect = reverse('horizon:nova:instances_and_volumes:index') + exceptions.handle(self.request, + _('Unable to retrieve details for ' + 'instance "%s".') % instance_id, + redirect=redirect) + self._instance = instance + return self._instance + + def get_tabs(self, request, *args, **kwargs): + instance = self.get_data() + return self.tab_group_class(request, instance=instance, **kwargs) diff --git a/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/_detail_log.html b/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/_detail_log.html new file mode 100644 index 000000000..ac3a8b7b5 --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/_detail_log.html @@ -0,0 +1,9 @@ +{% load i18n %} +
+ {% url horizon:nova:instances_and_volumes:instances:console instance.id as console_url %} + {% trans "View Full Log" %} +
+{{ console_log }}diff --git a/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/_detail_overview.html b/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/_detail_overview.html new file mode 100644 index 000000000..22460c77f --- /dev/null +++ b/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/_detail_overview.html @@ -0,0 +1,65 @@ +{% load i18n sizeformat %} + +
{% blocktrans %}If VNC console is not responding to keyboard input: click the grey status bar below.{% endblocktrans %}
+ diff --git a/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/detail.html b/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/detail.html index 2cc990cc8..a8eb48638 100644 --- a/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/detail.html +++ b/horizon/horizon/dashboards/nova/templates/nova/instances_and_volumes/instances/detail.html @@ -8,122 +8,30 @@ {% endblock page_header %} {% block dash_main %} - - -If VNC console is not responding to keyboard input: click the grey status bar below.
- -