From 406cb5d56cd9e3b8afbea45c209150707db7bcb3 Mon Sep 17 00:00:00 2001 From: John Postlethwait Date: Sat, 23 Jun 2012 13:58:35 -0700 Subject: [PATCH] Volume Progress Bar & Fixes For Quota When a volume creation exceed the allocation quota a vague error message was returned that offered the user no guidance as to what went wrong. This has been fixed. Fixes Bug #1012883 This change also abstracts the Quota javascript to allow it to be used anywhere on the site for any progress bars that are to be shown in the future. Implements blueprint progress-bar-javascript On top of this I have renamed all "can_haz" filters in the code, as they are really sort of embarassing. Lastly, I have added the ability to append JS events to the window load when a modal is loaded as a static page, OR when it is loaded as an AJAX modal. Change-Id: I4b0cefa160cafbbd07d4b0981f62febaed051871 --- horizon/api/nova.py | 18 +- .../snapshots/create.html | 2 - .../instances/_launch_details_help.html | 68 ++--- .../instances/_launch_volumes_help.html | 19 -- .../volumes/_create.html | 44 +++- .../instances_and_volumes/volumes/forms.py | 30 ++- .../instances_and_volumes/volumes/tests.py | 63 +++++ .../instances_and_volumes/volumes/views.py | 9 + horizon/exceptions.py | 3 +- horizon/static/horizon/js/horizon.quota.js | 244 ++++++++++++++++++ horizon/static/horizon/js/horizon.quotas.js | 69 ----- horizon/static/horizon/js/horizon.utils.js | 31 ++- horizon/templates/horizon/_nav_list.html | 2 +- horizon/templates/horizon/_scripts.html | 2 +- horizon/templates/horizon/_subnav_list.html | 2 +- .../horizon/client_side/_script_loader.html | 20 ++ horizon/templatetags/horizon.py | 7 +- horizon/tests/api_tests/nova_tests.py | 75 +++--- horizon/tests/templates/base.html | 1 + horizon/tests/testsettings.py | 1 + horizon/tests/workflows_tests.py | 2 +- horizon/workflows/base.py | 4 +- openstack_dashboard/settings.py | 1 + openstack_dashboard/templates/base.html | 1 + 24 files changed, 521 insertions(+), 197 deletions(-) create mode 100644 horizon/static/horizon/js/horizon.quota.js delete mode 100644 horizon/static/horizon/js/horizon.quotas.js create mode 100644 horizon/templates/horizon/client_side/_script_loader.html diff --git a/horizon/api/nova.py b/horizon/api/nova.py index de30ed3d7..113e39e00 100644 --- a/horizon/api/nova.py +++ b/horizon/api/nova.py @@ -407,18 +407,19 @@ def usage_list(request, start, end): def tenant_quota_usages(request): """ Builds a dictionary of current usage against quota for the current - tenant. + project. """ - # TODO(tres): Make this capture floating_ips and volumes as well. instances = server_list(request) floating_ips = tenant_floating_ip_list(request) quotas = tenant_quota_get(request, request.user.tenant_id) flavors = dict([(f.id, f) for f in flavor_list(request)]) + volumes = volume_list(request) + usages = {'instances': {'flavor_fields': [], 'used': len(instances)}, 'cores': {'flavor_fields': ['vcpus'], 'used': 0}, - 'gigabytes': {'used': 0, - 'flavor_fields': ['disk', - 'OS-FLV-EXT-DATA:ephemeral']}, + 'gigabytes': {'used': sum([int(v.size) for v in volumes]), + 'flavor_fields': []}, + 'volumes': {'used': len(volumes), 'flavor_fields': []}, 'ram': {'flavor_fields': ['ram'], 'used': 0}, 'floating_ips': {'flavor_fields': [], 'used': len(floating_ips)}} @@ -427,11 +428,18 @@ def tenant_quota_usages(request): for flavor_field in usages[usage]['flavor_fields']: usages[usage]['used'] += getattr( flavors[instance.flavor['id']], flavor_field, 0) + usages[usage]['quota'] = getattr(quotas, usage) + if usages[usage]['quota'] is None: usages[usage]['quota'] = float("inf") usages[usage]['available'] = float("inf") + elif type(usages[usage]['quota']) is str: + usages[usage]['quota'] = int(usages[usage]['quota']) else: + if type(usages[usage]['used']) is str: + usages[usage]['used'] = int(usages[usage]['used']) + usages[usage]['available'] = usages[usage]['quota'] - \ usages[usage]['used'] diff --git a/horizon/dashboards/nova/images_and_snapshots/templates/images_and_snapshots/snapshots/create.html b/horizon/dashboards/nova/images_and_snapshots/templates/images_and_snapshots/snapshots/create.html index 086d013b4..8dba1e41d 100644 --- a/horizon/dashboards/nova/images_and_snapshots/templates/images_and_snapshots/snapshots/create.html +++ b/horizon/dashboards/nova/images_and_snapshots/templates/images_and_snapshots/snapshots/create.html @@ -9,5 +9,3 @@ {% block dash_main %} {% include 'nova/images_and_snapshots/snapshots/_create.html' %} {% endblock %} - - diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_details_help.html b/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_details_help.html index 125475ecd..89362202e 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_details_help.html +++ b/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_details_help.html @@ -1,4 +1,4 @@ -{% load i18n horizon %} +{% load i18n horizon humanize %}

{% blocktrans %}Specify the details for launching an instance.{% endblocktrans %}

{% blocktrans %}The chart below shows the resources used by this project in relation to the project's quotas.{% endblocktrans %}

@@ -17,55 +17,37 @@

{% trans "Project Quotas" %}

-
- {% trans "Instance Count" %} ({{ usages.instances.used }}) -

{{ usages.instances.available|quota }}

+
+ {% trans "Number of Instances" %} ({{ usages.instances.used|intcomma }}) +

{{ usages.instances.available|quota|intcomma }}

+
+
+ {% horizon_progress_bar usages.instances.used usages.instances.quota %}
-
-
{% horizon_progress_bar usages.instances.used usages.instances.quota %}
-
- {% trans "VCPUs" %} ({{ usages.cores.used }}) -

{{ usages.cores.available|quota }}

+
+ {% trans "Number of VCPUs" %} ({{ usages.cores.used|intcomma }}) +

{{ usages.cores.available|quota|intcomma }}

+
+
+ {% horizon_progress_bar usages.cores.used usages.cores.quota %}
-
-
{% horizon_progress_bar usages.cores.used usages.cores.quota %}
-
- {% trans "Disk" %} ({{ usages.gigabytes.used }} {% trans "GB" %}) -

{{ usages.gigabytes.available|quota:"GB" }}

+
+ {% trans "Total Memory" %} ({{ usages.ram.used|intcomma }} {% trans "MB" %}) +

{{ usages.ram.available|quota:"MB"|intcomma }}

-
-
{% horizon_progress_bar usages.gigabytes.used usages.gigabytes.quota %}
- -
- {% trans "Memory" %} ({{ usages.ram.used }} {% trans "MB" %}) -

{{ usages.ram.available|quota:"MB" }}

+
+ {% horizon_progress_bar usages.ram.used usages.ram.quota %}
-
-
{% horizon_progress_bar usages.ram.used usages.ram.quota %}
diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_volumes_help.html b/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_volumes_help.html index 6d0f7d3a2..26f4429cb 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_volumes_help.html +++ b/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/instances/_launch_volumes_help.html @@ -1,22 +1,3 @@ {% load i18n horizon %}

{% blocktrans %}An instance can be launched with varying types of attached storage. You may select from those options here.{% endblocktrans %}

- - diff --git a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_create.html b/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_create.html index f0645485b..47f1adef2 100644 --- a/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_create.html +++ b/horizon/dashboards/nova/instances_and_volumes/templates/instances_and_volumes/volumes/_create.html @@ -1,5 +1,5 @@ {% extends "horizon/common/_modal_form.html" %} -{% load i18n %} +{% load i18n horizon humanize %} {% block form_id %}{% endblock %} {% block form_action %}{% url horizon:nova:instances_and_volumes:volumes:create %}{% endblock %} @@ -8,15 +8,47 @@ {% block modal-header %}{% trans "Create Volume" %}{% endblock %} {% block modal-body %} -
+
- {% include "horizon/common/_form_fields.html" %} + {% include "horizon/common/_form_fields.html" %}
-
-
+
+ +

{% trans "Description" %}:

+

{% trans "Volumes are block devices that can be attached to instances." %}

-
+ +

{% trans "Volume Quotas" %}

+ +
+ {% trans "Total Gigabytes" %} ({{ usages.gigabytes.used|intcomma }} GB) +

{{ usages.gigabytes.available|quota:"GB"|intcomma }}

+
+ +
+ {% horizon_progress_bar usages.gigabytes.used usages.gigabytes.quota %} +
+ +
+ {% trans "Number of Volumes" %} ({{ usages.volumes.used|intcomma }}) +

{{ usages.volumes.available|quota|intcomma }}

+
+ +
+ {% horizon_progress_bar usages.volumes.used usages.volumes.quota %} +
+
+ + {% endblock %} {% block modal-footer %} diff --git a/horizon/dashboards/nova/instances_and_volumes/volumes/forms.py b/horizon/dashboards/nova/instances_and_volumes/volumes/forms.py index 33f8dd6c7..b35c18170 100644 --- a/horizon/dashboards/nova/instances_and_volumes/volumes/forms.py +++ b/horizon/dashboards/nova/instances_and_volumes/volumes/forms.py @@ -9,6 +9,7 @@ Views for managing Nova volumes. from django import shortcuts from django.contrib import messages +from django.forms import ValidationError from django.utils.translation import ugettext_lazy as _ from horizon import api @@ -26,13 +27,38 @@ class CreateForm(forms.SelfHandlingForm): def handle(self, request, data): try: + # FIXME(johnp): Nova (cinderclient) currently returns a useless + # error message when the quota is exceeded when trying to create + # a volume, so we need to check for that scenario here before we + # send it off to Nova to try and create. + usages = api.tenant_quota_usages(request) + + if type(data['size']) is str: + data['size'] = int(data['size']) + + if usages['gigabytes']['available'] < data['size']: + error_message = _('A volume of %iGB cannot be created as you' + ' only have %iGB of your quota available.' + % (data['size'], + usages['gigabytes']['available'],)) + raise ValidationError(error_message) + elif usages['volumes']['available'] <= 0: + error_message = _('You are already using all of your available' + ' volumes.') + raise ValidationError(error_message) + api.volume_create(request, data['size'], data['name'], data['description']) message = 'Creating volume "%s"' % data['name'] + messages.info(request, message) + except ValidationError, e: + return self.api_error(e.messages[0]) except: - exceptions.handle(request, - _("Unable to create volume.")) + exceptions.handle(request, ignore=True) + + return self.api_error(_("Unable to create volume.")) + return shortcuts.redirect("horizon:nova:instances_and_volumes:index") diff --git a/horizon/dashboards/nova/instances_and_volumes/volumes/tests.py b/horizon/dashboards/nova/instances_and_volumes/volumes/tests.py index 88f59431b..bfdef759e 100644 --- a/horizon/dashboards/nova/instances_and_volumes/volumes/tests.py +++ b/horizon/dashboards/nova/instances_and_volumes/volumes/tests.py @@ -27,6 +27,69 @@ from horizon import test class VolumeViewTests(test.TestCase): + @test.create_stubs({api: ('tenant_quota_usages', 'volume_create',)}) + def test_create_volume(self): + usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}} + formData = {'name': u'A Volume I Am Making', + 'description': u'This is a volume I am making for a test.', + 'method': u'CreateForm', + 'size': 50} + + api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + api.volume_create(IsA(http.HttpRequest), + formData['size'], + formData['name'], + formData['description']) + + self.mox.ReplayAll() + + url = reverse('horizon:nova:instances_and_volumes:volumes:create') + res = self.client.post(url, formData) + + redirect_url = reverse('horizon:nova:instances_and_volumes:index') + self.assertRedirectsNoFollow(res, redirect_url) + + @test.create_stubs({api: ('tenant_quota_usages',)}) + def test_create_volume_gb_used_over_alloted_quota(self): + usage = {'gigabytes': {'available': 100, 'used': 20}} + formData = {'name': u'This Volume Is Huge!', + 'description': u'This is a volume that is just too big!', + 'method': u'CreateForm', + 'size': 5000} + + api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + + self.mox.ReplayAll() + + url = reverse('horizon:nova:instances_and_volumes:volumes:create') + res = self.client.post(url, formData) + + expected_error = [u'A volume of 5000GB cannot be created as you only' + ' have 100GB of your quota available.'] + self.assertEqual(res.context['form'].errors['__all__'], expected_error) + + @test.create_stubs({api: ('tenant_quota_usages',)}) + def test_create_volume_number_over_alloted_quota(self): + usage = {'gigabytes': {'available': 100, 'used': 20}, + 'volumes': {'available': 0}} + formData = {'name': u'Too Many...', + 'description': u'We have no volumes left!', + 'method': u'CreateForm', + 'size': 10} + + api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) + + self.mox.ReplayAll() + + url = reverse('horizon:nova:instances_and_volumes:volumes:create') + res = self.client.post(url, formData) + + expected_error = [u'You are already using all of your available' + ' volumes.'] + self.assertEqual(res.context['form'].errors['__all__'], expected_error) + @test.create_stubs({api: ('volume_get',), api.nova: ('server_list',)}) def test_edit_attachments(self): volume = self.volumes.first() diff --git a/horizon/dashboards/nova/instances_and_volumes/volumes/views.py b/horizon/dashboards/nova/instances_and_volumes/volumes/views.py index 03b74624b..63eb6de28 100644 --- a/horizon/dashboards/nova/instances_and_volumes/volumes/views.py +++ b/horizon/dashboards/nova/instances_and_volumes/volumes/views.py @@ -44,6 +44,15 @@ class CreateView(forms.ModalFormView): form_class = CreateForm template_name = 'nova/instances_and_volumes/volumes/create.html' + def get_context_data(self, **kwargs): + context = super(CreateView, self).get_context_data(**kwargs) + try: + context['usages'] = api.tenant_quota_usages(self.request) + except: + exceptions.handle(self.request) + + return context + class CreateSnapshotView(forms.ModalFormView): form_class = CreateSnapshotForm diff --git a/horizon/exceptions.py b/horizon/exceptions.py index 0c3a8c6fa..707b4fe62 100644 --- a/horizon/exceptions.py +++ b/horizon/exceptions.py @@ -57,7 +57,8 @@ class HorizonReporterFilter(SafeExceptionReporterFilter): sensitive_variables = None while current_frame is not None: if (current_frame.f_code.co_name == 'sensitive_variables_wrapper' - and 'sensitive_variables_wrapper' in current_frame.f_locals): + and 'sensitive_variables_wrapper' + in current_frame.f_locals): # The sensitive_variables decorator was used, so we take note # of the sensitive variables' names. wrapper = current_frame.f_locals['sensitive_variables_wrapper'] diff --git a/horizon/static/horizon/js/horizon.quota.js b/horizon/static/horizon/js/horizon.quota.js new file mode 100644 index 000000000..7dc9a27bd --- /dev/null +++ b/horizon/static/horizon/js/horizon.quota.js @@ -0,0 +1,244 @@ +/* + Used for animating and displaying quota information on forms which use the + Bootstrap progress bars. Also used for displaying flavor details on modal- + dialogs. + + Usage: + In order to have progress bars that work with this, you need to have a + DOM structure like this in your Django template: + +
+ {% horizon_progress_bar total_number_used max_number_allowed %} +
+ + With this progress bar, you then need to add some data- HTML attributes + to the div #your_progress_bar_id. The available data- attributes are: + + data-quota-used="integer" REQUIRED + Integer representing the total number used by the user. + + data-quota-limit="integer" REQUIRED + Integer representing the total quota limit the user has. Note this IS + NOT the amount remaining they can use, but the total original quota. + + ONE OF THE THREE ATTRIBUTES BELOW IS REQUIRED: + + data-progress-indicator-step-by="integer" OPTIONAL + Indicates the numeric unit the quota JavaScript should automatically + animate this progress bar by on load. Can be used with the other + data- attributes. + + A good use-case here is when you have a modal dialog to create ONE + volume, and you have a progress bar for volumes, but there are no + form elements that represent that number (as it is not settable by + the user.) + + data-progress-indicator-for="html_id_of_form_input" + Tells the quota JavaScript which form element on this page is tied to + this progress indicator. If this form element is an input, it will + automatically fire on "keyup" in that form field, and change this + progress bar to denote the numeric change. + + data-progress-indicator-flavor + This attribute is used to tell this quota JavaScript that this + progress bar is controller by an instance flavor select form element. + This attribute takes no value, but is used and configured + automatically by this script to update when a new flavor is choosen + by the end-user. + */ +horizon.Quota = { + is_flavor_quota: false, // Is this a flavor-based quota display? + user_value_progress_bars: [], // Progress bars triggered by user-changeable form elements. + auto_value_progress_bars: [], // Progress bars that should be automatically changed. + flavor_progress_bars: [], // Progress bars that relate to flavor details. + user_value_form_inputs: [], // The actual form inputs that trigger progress changes. + selected_flavor: null, // The flavor object of the current selected flavor on the form. + flavors: [], // The flavor objects the form represents, passed to us in initWithFlavors. + + /* + Determines the progress bars and form elements to be used for quota + display. Also attaches handlers to the form elements as well as performing + the animations when the progress bars first load. + */ + init: function() { + this.user_value_progress_bars = $('div[data-progress-indicator-for]'); + this.auto_value_progress_bars = $('div[data-progress-indicator-step-by]'); + this.user_value_form_inputs = $($.map(this.user_value_progress_bars, function(elm) { + return ('#' + $(elm).attr('data-progress-indicator-for')); + })); + + this._initialAnimations(); + this._attachInputHandlers(); + }, + + /* + Sets up the quota to be used with flavor form selectors, which requires + some different handling of the forms. Also calls init() so that all of the + other animations and handlers are taken care of as well when initializing + with this method. + */ + initWithFlavors: function(flavors) { + this.is_flavor_quota = true; + this.flavor_progress_bars = $('div[data-progress-indicator-flavor]'); + this.flavors = flavors; + + this.init(); + + this.showFlavorDetails(); + this.updateFlavorUsage(); + }, + + // Returns the flavor object for the selected flavor in the form. + getSelectedFlavor: function() { + if(this.is_flavor_quota) { + this.selected_flavor = _.find(this.flavors, function(flavor) { + return flavor.id == $("#id_flavor").children(":selected").val(); + }); + } else { + this.selected_flavor = null; + } + + return this.selected_flavor; + }, + + /* + Populates the flavor details table with the flavor attributes of the + selected flavor on the form select element. + */ + showFlavorDetails: function() { + this.getSelectedFlavor(); + + var name = horizon.utils.truncate(this.selected_flavor.name, 14, true); + var vcpus = horizon.utils.humanizeNumbers(this.selected_flavor.vcpus); + var disk = horizon.utils.humanizeNumbers(this.selected_flavor.disk); + var ephemeral = horizon.utils.humanizeNumbers(this.selected_flavor["OS-FLV-EXT-DATA:ephemeral"]); + var disk_total = this.selected_flavor.disk + this.selected_flavor["OS-FLV-EXT-DATA:ephemeral"]; + var disk_total_display = horizon.utils.humanizeNumbers(disk_total); + var ram = horizon.utils.humanizeNumbers(this.selected_flavor.ram); + + $("#flavor_name").html(name); + $("#flavor_vcpus").text(vcpus); + $("#flavor_disk").text(disk); + $("#flavor_ephemeral").text(ephemeral); + $("#flavor_disk_total").text(disk_total_display); + $("#flavor_ram").text(ram); + }, + + // Updates a progress bar, taking care of exceeding quota display as well. + update: function(element, percentage_used, percentage_to_update) { + var update_width = percentage_to_update; + + if(percentage_to_update + percentage_used > 100) { + update_width = 100 - percentage_used; + + if(!element.hasClass('progress_bar_over')) { + element.addClass('progress_bar_over'); + } + } else { + element.removeClass('progress_bar_over'); + } + + element.animate({width: update_width + "%"}, 300); + }, + + /* + When a new flavor is selected, this takes care of updating the relevant + progress bars associated with the flavor quota usage. + */ + updateFlavorUsage: function() { + if(!this.is_flavor_quota) return; + + var scope = this; + var instance_count = (parseInt($("#id_count").val(), 10) || 1); + var update_amount = 0; + + this.getSelectedFlavor(); + + $(this.flavor_progress_bars).each(function(index, element) { + var element_id = $(element).attr('id'); + var progress_stat = element_id.match(/^quota_(.+)/)[1]; + + if(progress_stat === undefined) { + return; + } else if(progress_stat === 'instances') { + update_amount = instance_count; + } else { + update_amount = (scope.selected_flavor[progress_stat] * instance_count); + } + + scope.updateUsageFor(element, update_amount); + }); + }, + + // Does the math to calculate what percentage to update a progress bar by. + updateUsageFor: function(progress_element, increment_by) { + progress_element = $(progress_element); + + var update_indicator = progress_element.find('.progress_bar_selected'); + var quota_limit = parseInt(progress_element.attr('data-quota-limit'), 10); + var quota_used = parseInt(progress_element.attr('data-quota-used'), 10); + var percentage_to_update = ((increment_by / quota_limit) * 100); + var percentage_used = ((quota_used / quota_limit) * 100); + + this.update(update_indicator, percentage_used, percentage_to_update); + }, + + /* + Attaches event handlers for the form elements associated with the + progress bars. + */ + _attachInputHandlers: function() { + var scope = this; + + if(this.is_flavor_quota) { + var eventCallback = function(evt) { + scope.showFlavorDetails(); + scope.updateFlavorUsage(); + }; + + $('#id_flavor').on('change', eventCallback); + $('#id_count').on('keyup', eventCallback); + } + + $(this.user_value_form_inputs).each(function(index, element) { + $(element).on('keyup', function(evt) { + var progress_element = $('div[data-progress-indicator-for=' + $(evt.target).attr('id') + ']'); + var integers_in_input = $(evt.target).val().match(/\d+/g); + var user_integer; + + if(integers_in_input === null) { + user_integer = 0; + } else if(integers_in_input.length > 1) { + /* + Join all the numbers together that have been typed in. This takes + care of junk input like "dd8d72n3k" and uses just the digits in + that input, resulting in "8723". + */ + user_integer = integers_in_input.join(''); + } else if(integers_in_input.length == 1) { + user_integer = integers_in_input[0]; + } + + var progress_amount = parseInt(user_integer, 10); + + scope.updateUsageFor(progress_element, progress_amount); + }); + }); + }, + + /* + Animate the progress bars of elements which indicate they should + automatically be incremented, as opposed to elements which trigger + progress updates based on form element input or changes. + */ + _initialAnimations: function() { + var scope = this; + + $(this.auto_value_progress_bars).each(function(index, element) { + var auto_progress = $(element); + var update_amount = parseInt(auto_progress.attr('data-progress-indicator-step-by'), 10); + + scope.updateUsageFor(auto_progress, update_amount); + }); + } +}; diff --git a/horizon/static/horizon/js/horizon.quotas.js b/horizon/static/horizon/js/horizon.quotas.js deleted file mode 100644 index 49b8b87e2..000000000 --- a/horizon/static/horizon/js/horizon.quotas.js +++ /dev/null @@ -1,69 +0,0 @@ -/* Update quota usage infographics when a flavor is selected to show the usage - * that will be consumed by the selected flavor. */ -horizon.updateQuotaUsages = function(flavors, usages) { - var selectedFlavor = _.find(flavors, function(flavor) { - return flavor.id == $("#id_flavor").children(":selected").val(); - }); - - var selectedCount = parseInt($("#id_count").val(), 10); - if(isNaN(selectedCount)) { - selectedCount = 1; - } - - // Map usage data fields to their corresponding html elements - var flavorUsageMapping = [ - {'usage': 'instances', 'element': 'quota_instances'}, - {'usage': 'cores', 'element': 'quota_cores'}, - {'usage': 'gigabytes', 'element': 'quota_disk'}, - {'usage': 'ram', 'element': 'quota_ram'} - ]; - - var el, used, usage, width; - _.each(flavorUsageMapping, function(mapping) { - el = $('#' + mapping.element + " .progress_bar_selected"); - used = 0; - usage = usages[mapping.usage]; - - if(mapping.usage == "instances") { - used = selectedCount; - } else { - _.each(usage.flavor_fields, function(flavorField) { - used += (selectedFlavor[flavorField] * selectedCount); - }); - } - - available = 100 - $('#' + mapping.element + " .progress_bar_fill").attr("data-width"); - if(used + usage.used <= usage.quota) { - width = Math.round((used / usage.quota) * 100); - el.removeClass('progress_bar_over'); - } else { - width = available; - if(!el.hasClass('progress_bar_over')) { - el.addClass('progress_bar_over'); - } - } - - el.animate({width: width + "%"}, 300); - }); - - // Also update flavor details - $("#flavor_name").html(horizon.utils.truncate(selectedFlavor.name, 14, true)); - $("#flavor_vcpus").text(selectedFlavor.vcpus); - $("#flavor_disk").text(selectedFlavor.disk); - $("#flavor_ephemeral").text(selectedFlavor["OS-FLV-EXT-DATA:ephemeral"]); - $("#flavor_disk_total").text(selectedFlavor.disk + selectedFlavor["OS-FLV-EXT-DATA:ephemeral"]); - $("#flavor_ram").text(selectedFlavor.ram); -}; - -horizon.addInitFunction(function () { - var quota_containers = $(".quota-dynamic"); - if (quota_containers.length) { - horizon.updateQuotaUsages(horizon_flavors, horizon_usages); - } - $(document).on("change", "#id_flavor", function() { - horizon.updateQuotaUsages(horizon_flavors, horizon_usages); - }); - $(document).on("keyup", "#id_count", function() { - horizon.updateQuotaUsages(horizon_flavors, horizon_usages); - }); -}); diff --git a/horizon/static/horizon/js/horizon.utils.js b/horizon/static/horizon/js/horizon.utils.js index 376549b51..bc361167a 100644 --- a/horizon/static/horizon/js/horizon.utils.js +++ b/horizon/static/horizon/js/horizon.utils.js @@ -3,14 +3,33 @@ horizon.utils = { capitalize: function(string) { return string.charAt(0).toUpperCase() + string.slice(1); }, - // Truncate a string at the desired length + + /* + Adds commas to any integer or numbers within a string for human display. + + EG: + horizon.utils.humanizeNumbers(1234); -> "1,234" + horizon.utils.humanizeNumbers("My Total: 1234"); -> "My Total: 1,234" + */ + humanizeNumbers: function(number) { + return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + }, + + /* + Truncate a string at the desired length. Optionally append an ellipsis + to the end of the string. + + EG: + horizon.utils.truncate("String that is too long.", 18, true); -> + "String that is too…" + */ truncate: function(string, size, includeEllipsis) { - var ellip = ""; - if(includeEllipsis) { - ellip = "…"; - } if(string.length > size) { - return string.substring(0, size) + ellip; + if(includeEllipsis) { + return string.substring(0, (size - 3)) + "…"; + } else { + return string.substring(0, size); + } } else { return string; } diff --git a/horizon/templates/horizon/_nav_list.html b/horizon/templates/horizon/_nav_list.html index b7f57e279..f300be27a 100644 --- a/horizon/templates/horizon/_nav_list.html +++ b/horizon/templates/horizon/_nav_list.html @@ -3,7 +3,7 @@