summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLadislav Smola <lsmola@redhat.com>2013-07-11 15:14:30 +0200
committerTomas Sedovic <tomas@sedovic.cz>2013-08-01 16:18:56 +0200
commitbdcd93ca01e179dd745a91db3f85b66bc94cb3e8 (patch)
treea24415c80e50c1a952e59955465f965f5438e9bc
parent936eb61b66c9d0c8590698f394994385361d9a7d (diff)
downloadtuskar-ui-bdcd93ca01e179dd745a91db3f85b66bc94cb3e8.tar.gz
Adding bar charts
- adding general bar chart - configuration for width, height, horizontal/vertical, multiple values - configuration for popup message - configuration for color scales Change-Id: I89f0bfe24155be4ba9e2f9fa0e1406ce1e0bc3cc
-rw-r--r--horizon/static/horizon/js/horizon.d3singlebarchart.js472
-rw-r--r--horizon/templates/horizon/_scripts.html1
-rw-r--r--openstack_dashboard/api/tuskar.py43
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tabs.py5
-rw-r--r--openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_overview.html59
-rw-r--r--openstack_dashboard/dashboards/infrastructure/static/infrastructure/less/infrastructure.less22
-rw-r--r--openstack_dashboard/dashboards/infrastructure/templates/infrastructure/base_detail.html11
-rw-r--r--openstack_dashboard/dashboards/infrastructure/templatetags/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/infrastructure/templatetags/chart_helpers.py40
9 files changed, 647 insertions, 6 deletions
diff --git a/horizon/static/horizon/js/horizon.d3singlebarchart.js b/horizon/static/horizon/js/horizon.d3singlebarchart.js
new file mode 100644
index 00000000..c17415bb
--- /dev/null
+++ b/horizon/static/horizon/js/horizon.d3singlebarchart.js
@@ -0,0 +1,472 @@
+/*
+ Used for animating and displaying single-bar information using
+ D3js rect.
+
+ Usage:
+ In order to have single bars that work with this, you need to have a
+ DOM structure like this in your Django template:
+
+ Example:
+ <div class="flavor_usage_bar"
+ data-popup-free='<p>Capacity remaining by flavors: </p>
+ {{resource_class.all_instances_flavors_info}}'
+ data-single-bar-orientation="horizontal"
+ data-single-bar-height="50"
+ data-single-bar-width="100%"
+ data-single-bar-used="{{ resource_class.all_used_instances_info }}"
+ data-single-bar-auto-scale-selector=".flavors_scale_selector"
+ data-single-bar-color-scale-range='["#000060", "#99FFFF"]'>
+ </div>
+
+ The available data- attributes are:
+ data-popup-free, data-popup-used, data-popup-average OPTIONAL
+ Html content of popups that will be displayed over this areas.
+
+ data-single-bar-orientation REQUIRED
+ String representing orientation of the bar.Can be "horizontal"
+ or "vertical"
+
+ data-single-bar-height REQUIRED
+ Integer or string with percent mark format e.g. "50%". Determines
+ the total height of the bar.
+
+ data-single-bar-width="100%" REQUIRED
+ Integer or string with percent mark format e.g. "50%". Determines
+ the total width of the bar.
+
+ data-single-bar-used="integer" REQUIRED
+ 1. Integer
+ Integer representing the percent used.
+ 2. Array
+ Aray of following structure:
+ [{"popup_used": "Popup html 1", "used_instances": "5"},
+ {"popup_used": "Popup html 2", "used_instances": "15"},....]
+
+ used_instances: Integer representing the percent used.
+ popup_used: Html that will be displayed in popup window over
+ this area.
+
+ data-single-bar-used-average="integer" OPTIONAL
+ Integer representing the average usage in percent of given
+ single-bar.
+
+ data-single-bar-auto-scale-selector OPTIONAL
+ Jquery selector of bar elements that have Integer
+ data-single-bar-used attribute. It then takes maximum of these
+ values as 100% of the liner scale of the colors.
+ So the array representing linear scale interval is set
+ automatically.This then maps to data-single-bar-color-scale-range.
+ (arays must have the same structure)
+
+ single-bar-color-scale-range OPTIONAL
+ Array representing linear scale interval that is set manually.
+ E.g "[0,10]". This then maps to data-single-bar-color-scale-range.
+ (arays must have the same structure)
+
+ data-single-bar-color-scale-range OPTIONAL
+ Array representing linear scale of colors.
+ E.g '["#000060", "#99FFFF"]'
+
+*/
+
+horizon.d3_single_bar_chart = {
+ SingleBarChart: function(chart_class, html_element){
+ var self = this;
+ self.chart_class = chart_class;
+
+ self.html_element = html_element;
+ self.jquery_element = $(self.html_element);
+ // Using only percent, so limit is 100%
+ self.single_bar_limit = 100;
+
+ self.single_bar_used = $.parseJSON(self.jquery_element.attr('data-single-bar-used'));
+ self.average_used = parseInt(self.jquery_element.attr('data-single-bar-average-used'), 10);
+
+ self.data = {};
+
+ // Percentage and used_px count
+ if ($.isArray(self.single_bar_used)){
+ self.data.used_px = 0;
+ self.data.percentage_used = Array();
+ self.data.tooltip_used_contents = Array();
+ for (var i = 0; i < self.single_bar_used.length; ++i) {
+ if (!isNaN(self.single_bar_limit) && !isNaN(self.single_bar_used[i].used_instances)) {
+ used = Math.round((self.single_bar_used[i].used_instances / self.single_bar_limit) * 100);
+
+ self.data.percentage_used.push(used);
+ // for multi-bar chart, tooltip is in the data
+ self.data.tooltip_used_contents.push(self.single_bar_used[i].popup_used);
+
+ self.data.used_px += self.jquery_element.width() / 100 * used;
+ } else { // If NaN self.data.percentage_used is 0
+
+ }
+ }
+
+ }
+ else {
+ if (!isNaN(self.single_bar_limit) && !isNaN(self.single_bar_used)) {
+ self.data.percentage_used = Math.round((self.single_bar_used / self.single_bar_limit) * 100);
+ self.data.used_px = self.jquery_element.width() / 100 * self.data.percentage_used;
+
+ } else { // If NaN self.data.percentage_used is 0
+ self.data.percentage_used = 0;
+ self.data.used_px = 0;
+ }
+
+ if (!isNaN(self.single_bar_limit) && !isNaN(self.average_used)) {
+ self.data.percentage_average = ((self.average_used / self.single_bar_limit) * 100);
+ } else {
+ self.data.percentage_average = 0;
+ }
+ }
+
+ // Width and height of bar
+ self.data.width = self.jquery_element.data('single-bar-width');
+ self.data.height = self.jquery_element.data('single-bar-height');
+
+ // Color scales
+ self.auto_scale_selector = function () {
+ return self.jquery_element.data('single-bar-auto-scale-selector');
+ };
+ self.is_auto_scaling = function () {
+ return self.auto_scale_selector();
+ };
+ self.auto_scale = function () {
+ max_scale = 0;
+ $(self.auto_scale_selector()).each(function() {
+ var scale = parseInt($(this).data('single-bar-used'));
+ if (scale > max_scale)
+ max_scale = scale;
+ });
+ return [0, max_scale];
+ };
+
+ if (self.jquery_element.data('single-bar-color-scale-domain'))
+ self.data.color_scale_domain =
+ self.jquery_element.data('single-bar-color-scale-domain');
+ else if (self.is_auto_scaling())
+ // Dynamically set scale based on biggest value
+ self.data.color_scale_domain = self.auto_scale();
+ else
+ self.data.color_scale_domain = [0,100];
+
+ if (self.jquery_element.data('single-bar-color-scale-range'))
+ self.data.color_scale_range =
+ self.jquery_element.data('single-bar-color-scale-range');
+ else
+ self.data.color_scale_range = ["#000000", "#0000FF"];
+
+ // Tooltips data
+ self.data.popup_average = self.jquery_element.data('popup-average');
+ self.data.popup_free = self.jquery_element.data('popup-free');
+ self.data.popup_used = self.jquery_element.data('popup-used');
+
+ // Orientation of the Bar chart
+ self.data.orientation = self.jquery_element.data('single-bar-orientation');
+
+ // Refresh method
+ self.refresh = function (){
+ self.chart_class.render(self.html_element, self.data);
+ };
+ },
+ BaseComponent: function(data){
+ var self = this;
+
+ self.data = data;
+
+ self.w = data.width;
+ self.h = data.height;
+ self.lvl_curve = 3;
+ self.bkgrnd = "#F2F2F2";
+ self.frgrnd = "grey";
+ self.color_scale_max = 25;
+
+ self.percentage_used = data.percentage_used;
+ self.total_used_perc = 0;
+ self.used_px = data.used_px;
+ self.percentage_average = data.percentage_average;
+ self.tooltip_used_contents = data.tooltip_used_contents;
+
+ // set scales
+ self.usage_color = d3.scale.linear()
+ .domain(data.color_scale_domain)
+ .range(data.color_scale_range);
+
+ // return true if it renders used percentage multiple in one chart
+ self.used_multi = function (){
+ return ($.isArray(self.percentage_used));
+ };
+
+ // deals with percentage if there should be multiple in one chart
+ self.used_multi_iterator = 0;
+ self.percentage_used_value = function(){
+ if (self.used_multi()){
+ return self.percentage_used[self.used_multi_iterator];
+ } else {
+ return self.percentage_used;
+ }
+ };
+ // deals with html tooltips if there should be multiple in one chart
+ self.tooltip_used_value = function (){
+ if (self.used_multi()){
+ return self.tooltip_used_contents[self.used_multi_iterator];
+ } else
+ {
+ return "";
+ }
+ };
+
+ // return true if it chart is oriented horizontally
+ self.horizontal_orientation = function (){
+ return (self.data.orientation == "horizontal");
+ };
+
+ },
+ UsedComponent: function(base_component){
+ var self = this;
+ self.base_component = base_component;
+
+ // FIXME woud be good to abstract all atributes and resolve orientation inside
+ if (base_component.horizontal_orientation()){
+ // Horizontal Bars
+ self.y = 0;
+ self.x = base_component.total_used_perc + "%";
+ self.width = 0;
+ self.height = base_component.h;
+ self.trasition_attr = "width";
+ self.trasition_value = base_component.percentage_used_value() + "%";
+ } else
+ { // Vertical Bars
+ self.y = base_component.h;
+ self.x = 0;
+ self.width = base_component.w;
+ self.height = base_component.percentage_used_value() + "%";
+ self.trasition_attr = "y";
+ self.trasition_value = 100 - base_component.percentage_used_value() + "%";
+ }
+
+ self.append = function(bar, tooltip){
+ var used_component = self;
+ var base_component = self.base_component;
+
+ bar.append("rect")
+ .attr("class", "usedbar")
+ .attr("y", used_component.y)
+ .attr("x", used_component.x)
+ .attr("width", used_component.width)
+ .attr("height", used_component.height)
+ //.attr("rx", base_component.lvl_curve)
+ //.attr("ry", base_component.lvl_curve)
+ .style("fill", base_component.usage_color(base_component.percentage_used_value()))
+ .style("stroke", "#000000")
+ .style("stroke-width", 0)
+ .attr("d", base_component.percentage_used_value())
+ .attr("popup-used", base_component.tooltip_used_value())
+ .on("mouseover", function(d){
+ if ($(this).attr('popup-used')){
+ tooltip.html($(this).attr('popup-used'));
+ }
+ tooltip.style("visibility", "visible");})
+ .on("mousemove", function(d){tooltip.style("top",
+ (event.pageY-10)+"px").style("left",(event.pageX+10)+"px");})
+ .on("mouseout", function(d){tooltip.style("visibility", "hidden");})
+ .transition()
+ .duration(500)
+ .attr(used_component.trasition_attr, used_component.trasition_value);
+ };
+ },
+ AverageComponent: function(base_component){
+ var self = this;
+ self.base_component = base_component;
+
+ // FIXME woud be good to abstract all atributes and resolve orientation inside
+ if (base_component.horizontal_orientation()){
+ // Horizontal Bars
+ self.y = 1;
+ self.x = 0;
+ self.width = 1;
+ self.height = base_component.h;
+ self.trasition_attr = "x";
+ self.trasition_value = base_component.percentage_average + "%";
+ } else
+ { // Vertical Bars
+ self.y = 0;
+ self.x = 0;
+ self.width = base_component.w;
+ self.height = 3;
+ self.trasition_attr = "y";
+ self.trasition_value = 100 - base_component.percentage_average + "%";
+ }
+
+ self.append = function(bar, tooltip){
+ var average_component = self;
+ var base_component = self.base_component;
+
+ bar.append("rect")
+ .attr("y", average_component.y)
+ .attr("x", average_component.x)
+ .attr("class", "average")
+ .attr("height", average_component.height)
+ .attr("width", average_component.width)
+ .style("fill", "black")
+ .on("mouseover", function(){tooltip.style("visibility", "visible");})
+ .on("mousemove", function(){tooltip.style("top",
+ (event.pageY-10)+"px").style("left",(event.pageX+10)+"px");})
+ .on("mouseout", function(){tooltip.style("visibility", "hidden");})
+ .transition()
+ .duration(500)
+ .attr(average_component.trasition_attr, average_component.trasition_value);
+ };
+ },
+ /* TODO rewrite below as components */
+ /* FIXME use some pretty tooltip from some library we will use
+ * this one is just temporary */
+ append_tooltip: function(tooltip, html_content){
+ return tooltip
+ .style("position", "absolute")
+ .style("z-index", "10")
+ .style("visibility", "hidden")
+ .style("min-width", "100px")
+ .style("max-width", "200px")
+ .style("min-height", "30px")
+ .style("max-height", "150px")
+ .style("border", "1px ridge grey")
+ .style("padding", "8px")
+ .style("padding-top", "5px")
+ .style("background-color", "white")
+ .html(html_content);
+ },
+ append_unused: function(bar, base_component, tooltip_free){
+ bar.append("rect")
+ .attr("y", 0)
+ .attr("width", base_component.w)
+ .attr("height", base_component.h)
+ .attr("rx", base_component.lvl_curve)
+ .attr("ry", base_component.lvl_curve)
+ .style("fill", base_component.bkgrnd)
+ .style("stroke", "#000000")
+ .style("stroke-width", 1)
+ .on("mouseover", function(d){tooltip_free.style("visibility", "visible");})
+ .on("mousemove", function(d){tooltip_free.style("top",
+ (event.pageY-10)+"px").style("left",(event.pageX+10)+"px");})
+ .on("mouseout", function(d){tooltip_free.style("visibility", "hidden");});
+ },
+ // TODO This have to be enhanced, so this library can replace jtomasek capacity charts
+ append_text: function(bar, base_component, tooltip){
+ bar.append("text")
+ .text("FREE")
+ .attr("y", base_component.h/2)
+ .attr("x", 3)
+ .attr("dominant-baseline", "middle")
+ .attr("font-size", 15)
+ .on("mouseover", function(d){tooltip.style("visibility", "visible");})
+ .on("mousemove", function(d){tooltip.style("top",
+ (event.pageY-10)+"px").style("left",(event.pageX+10)+"px");})
+ .on("mouseout", function(d){tooltip.style("visibility", "hidden");})
+ .transition()
+ .duration(500)
+ .attr("x", function() {
+ // FIXME when another panel is active, this page is hidden and used_px return 0
+ // text is then badly positioned, quick fix will be to refresh charts when panel
+ // is switched. Need to find better solution.
+ if (base_component.total_used_perc > 90 && base_component.used_px > 25)
+ return base_component.used_px - 20;
+ else
+ return base_component.used_px + 20;
+ });
+ },
+ append_border: function(bar){
+ bar.append("rect")
+ .attr("x", 0)
+ .attr("y", 0)
+ .attr("height", '100%')
+ .attr("width", '100%')
+ .style("stroke", "black")
+ .style("fill", "none")
+ .style("stroke-width", 2);
+ },
+ // INIT
+ init: function() {
+ var self = this;
+ this.single_bars = $('div[data-single-bar-used]');
+
+ this.single_bars.each(function() {
+ self.refresh(this);
+ });
+ },
+ refresh: function(html_element){
+ var chart = new this.SingleBarChart(this, html_element);
+ // FIXME save chart objects somewhere so I can use them again when
+ // e.g. I am swithing tabs, or if I want to update them
+ // via web sockets
+ // this.charts.add_or_update(chart)
+ chart.refresh();
+ },
+ render: function(html_element, data) {
+ var jquery_element = $(html_element);
+
+ // Initialize base_component
+ var base_component = new this.BaseComponent(data);
+
+ // Bar
+ var bar_html = d3.select(html_element);
+
+ // Tooltips
+ var tooltip_average = bar_html.append("div");
+ if (data.popup_average)
+ tooltip_average = this.append_tooltip(tooltip_average, data.popup_average);
+
+ var tooltip_free = bar_html.append("div");
+ if (data.popup_free)
+ tooltip_free = this.append_tooltip(tooltip_free, data.popup_free);
+
+ var tooltip_used = bar_html.append("div");
+ if (data.popup_used)
+ tooltip_used = this.append_tooltip(tooltip_used, data.popup_used);
+
+ // append layout for bar chart
+ var bar = bar_html.append("svg:svg")
+ .attr("class", "chart")
+ .attr("width", base_component.w)
+ .attr("height", base_component.h)
+ .style("background-color", "white")
+ .append("g");
+
+ // append Unused resources Bar
+ this.append_unused(bar, base_component, tooltip_free);
+
+ if (base_component.used_multi()){
+ // If Used is shown as multiple values in one chart
+ for (var i = 0; i < base_component.percentage_used.length; ++i) {
+ // FIXME write proper iterator
+ base_component.used_multi_iterator = i;
+
+ // Use general tooltip, content of tooltip will be changed
+ // by inner used compoentnts on their hover
+ tooltip_used = this.append_tooltip(tooltip_used, "");
+
+ // append used so it will be shown as multiple values in one chart
+ var used_component = new this.UsedComponent(base_component);
+ used_component.append(bar, tooltip_used);
+
+ // append Used resources to Bar
+ base_component.total_used_perc += base_component.percentage_used_value();
+ };
+
+ // append Text to Bar
+ this.append_text(bar, base_component, tooltip_free);
+
+ } else {
+ // used is show as one value it the chart
+ var used_component = new this.UsedComponent(base_component);
+ used_component.append(bar, tooltip_used);
+
+ // append average value to Bar
+ var average_component = new this.AverageComponent(base_component);
+ average_component.append(bar, tooltip_average);
+ }
+ // append border of whole Bar
+ this.append_border(bar);
+ },
+};
diff --git a/horizon/templates/horizon/_scripts.html b/horizon/templates/horizon/_scripts.html
index 2f9ebce6..9dfd0f4c 100644
--- a/horizon/templates/horizon/_scripts.html
+++ b/horizon/templates/horizon/_scripts.html
@@ -43,6 +43,7 @@
<script src='{{ STATIC_URL }}horizon/js/horizon.d3modallinechart.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.d3circleschart.js' type='text/javascript' charset='utf-8'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.d3linechart.js' type='text/javascript' charset='utf-8'></script>
+<script src='{{ STATIC_URL }}horizon/js/horizon.d3singlebarchart.js' type='text/javascript' charset='utf-8'></script>
{% block custom_js_files %}{% endblock %}
{% endcompress %}
diff --git a/openstack_dashboard/api/tuskar.py b/openstack_dashboard/api/tuskar.py
index 3f09219d..12b4c393 100644
--- a/openstack_dashboard/api/tuskar.py
+++ b/openstack_dashboard/api/tuskar.py
@@ -457,11 +457,37 @@ class ResourceClass(StringIdAPIResourceWrapper):
@property
def list_flavors(self):
if not hasattr(self, '_flavors'):
- self._flavors = [Flavor(f) for f in (
- tuskarclient(self.request).flavors.list(self.id))]
+ # FIXME just a mock of used instances, add real values
+ used_instances = 0
+
+ added_flavors = tuskarclient(self.request).flavors.list(self.id)
+ self._flavors = []
+ for f in added_flavors:
+ flavor_obj = Flavor(f)
+ #flavor_obj.max_vms = f.max_vms
+
+ # FIXME just a mock of used instances, add real values
+ used_instances += 5
+ flavor_obj.used_instances = used_instances
+ self._flavors.append(flavor_obj)
+
return self._flavors
@property
+ def all_used_instances(self):
+ return [flavor.used_instances for flavor in self.list_flavors]
+
+ @property
+ def total_instances(self):
+ # FIXME just mock implementation, add proper one
+ return sum(self.all_used_instances)
+
+ @property
+ def remaining_capacity(self):
+ # FIXME just mock implementation, add proper one
+ return 100 - self.total_instances
+
+ @property
def all_flavors(self):
""" Joined relation table resourceclassflavor with all global flavors
"""
@@ -472,7 +498,7 @@ class ResourceClass(StringIdAPIResourceWrapper):
fname = "%s.%s" % (self.name, flavor.name)
f = next((f for f in my_flavors if f.name == fname), None)
if f:
- flavor.set_max_vms(f.max_vms)
+ flavor.max_vms = f.max_vms
self._all_flavors.append(flavor)
return self._all_flavors
@@ -611,9 +637,18 @@ class FlavorTemplate(StringIdAPIResourceWrapper):
def max_vms(self):
return getattr(self, '_max_vms', '0')
- def set_max_vms(self, value='0'):
+ @max_vms.setter
+ def max_vms(self, value='0'):
self._max_vms = value
+ @property
+ def used_instances(self):
+ return getattr(self, '_used_instances', 0)
+
+ @used_instances.setter
+ def used_instances(self, value=0):
+ self._used_instances = value
+
@classmethod
def list(cls, request, only_free_racks=False):
return [cls(f) for f in dummymodels.FlavorTemplate.objects.all()]
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tabs.py b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tabs.py
index 0909e064..0b667cad 100644
--- a/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tabs.py
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/resource_classes/tabs.py
@@ -24,8 +24,9 @@ class OverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = ("infrastructure/resource_management/resource_classes/"
- "_detail_overview.html")
- preload = False
+ "_detail_overview.html")
+ # FIXME charts doesnt work if uncommented
+ #preload = False
def get_context_data(self, request):
return {"resource_class": self.tab_group.kwargs['resource_class']}
diff --git a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_overview.html b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_overview.html
index 60570dd7..0c8fbb2e 100644
--- a/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_overview.html
+++ b/openstack_dashboard/dashboards/infrastructure/resource_management/templates/resource_management/resource_classes/_detail_overview.html
@@ -1,5 +1,6 @@
{% load i18n sizeformat %}
{% load url from future %}
+{% load chart_helpers %}
<div class="status row-fluid detail">
<div class="span4">
@@ -118,12 +119,70 @@
<div class="span6">
<h4>{% trans "Summary of Instances and Usage" %}</h4>
<hr class="header_rule">
+ <!--
<dl>
{% for flavor_count in resource_class.running_virtual_machines %}
<dt>{{ flavor_count.flavor.name }}</dt>
<dd>{{ flavor_count.max_vms }}</dd>
{% endfor %}
</dl>
+ -->
+
+ <div class="clear"></div>
+ <div><strong>{{ resource_class.total_instances }}</strong> instances
+ <strong>{{resource_class.remaining_capacity}}%</strong> capacity remaining</div>
+ <div class="flavor_usage_bar"
+ data-popup-free='{{resource_class|remaining_capacity_by_flavors}}'
+ data-single-bar-orientation="horizontal"
+ data-single-bar-height="50"
+ data-single-bar-width="100%"
+ data-single-bar-used="{{ resource_class|all_used_instances }}"
+ data-single-bar-auto-scale-selector=".flavors_scale_selector"
+ data-single-bar-color-scale-range='["#000060", "#99FFFF"]'
+ >
+ </div>
+
+ <table class="flavor_usages">
+ <tr>
+ {% for flavor in resource_class.list_flavors %}
+ <td class="flavor_usage_label">
+ <a href="{% url 'horizon:infrastructure:resource_management:flavors:detail' flavor.id %}">{{ flavor.name }}</a>
+ </td>
+ {% endfor %}
+ </tr>
+ <tr>
+ {% for flavor in resource_class.list_flavors %}
+ <td>
+ <div
+ class="flavor_usage_bar flavors_scale_selector"
+ data-popup-average='
+ <p>Average capacity consumed by instances of {{flavor.name}} flavor in {{resource_class.name}} class.</p>
+ <p>{{ flavor.used_instances }}%, <strong>{{ flavor.used_instances }} instances</strong></p>
+ '
+ data-single-bar-orientation="vertical"
+ data-single-bar-height="100%"
+ data-single-bar-width="40"
+ data-single-bar-used="{{ flavor.used_instances }}"
+ data-single-bar-average-used="{{ 50 }}"
+ data-single-bar-auto-scale-selector=".flavors_scale_selector"
+ data-single-bar-color-scale-range='["#000060", "#99FFFF"]'
+ >
+ </div>
+ </td>
+ {% endfor %}
+ </tr>
+ <tr>
+ {% for flavor in resource_class.list_flavors %}
+ <td class="modal_chart flavor_usage_text"><a href="{{ "#" }}">{{ flavor.used_instances }}%</a></td>
+ {% endfor %}
+ </tr>
+ <tr>
+ {% for flavor in resource_class.list_flavors %}
+ <td class="flavor_usage_text">{{ flavor.used_instances }} inst.</td>
+ {% endfor %}
+ </tr>
+ </table>
+
</div>
<div class="span6">
diff --git a/openstack_dashboard/dashboards/infrastructure/static/infrastructure/less/infrastructure.less b/openstack_dashboard/dashboards/infrastructure/static/infrastructure/less/infrastructure.less
index ec6b0a8d..7fe8cba5 100644
--- a/openstack_dashboard/dashboards/infrastructure/static/infrastructure/less/infrastructure.less
+++ b/openstack_dashboard/dashboards/infrastructure/static/infrastructure/less/infrastructure.less
@@ -253,6 +253,28 @@ table.capacities{
}
}
+table.flavor_usages{
+ width: 100%;
+ td{
+ text-align: center;
+ padding: 3px;
+ &.flavor_usage_label{
+ font-size: 17px;
+ width: 60px;
+ text-align: center;
+ }
+ &.flavor_usage_text{
+ width: 60px;
+ text-align: center;
+ }
+ & div.flavor_usage_bar{
+ text-align: center;
+ line-height: 0;
+ height: 150px;
+ }
+ }
+}
+
#interval_selector {
position: absolute;
diff --git a/openstack_dashboard/dashboards/infrastructure/templates/infrastructure/base_detail.html b/openstack_dashboard/dashboards/infrastructure/templates/infrastructure/base_detail.html
index f7274ea7..daf3f850 100644
--- a/openstack_dashboard/dashboards/infrastructure/templates/infrastructure/base_detail.html
+++ b/openstack_dashboard/dashboards/infrastructure/templates/infrastructure/base_detail.html
@@ -24,4 +24,15 @@
</div>
</div>
+<script type="text/javascript" charset="utf-8">
+
+ if(typeof horizon.d3_single_bar_chart !== 'undefined') {
+ horizon.d3_single_bar_chart.init();
+ } else {
+ addHorizonLoadEvent(function() {
+ horizon.d3_single_bar_chart.init();
+ });
+ }
+
+</script>
{% endblock %}
diff --git a/openstack_dashboard/dashboards/infrastructure/templatetags/__init__.py b/openstack_dashboard/dashboards/infrastructure/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/templatetags/__init__.py
diff --git a/openstack_dashboard/dashboards/infrastructure/templatetags/chart_helpers.py b/openstack_dashboard/dashboards/infrastructure/templatetags/chart_helpers.py
new file mode 100644
index 00000000..8eefaf10
--- /dev/null
+++ b/openstack_dashboard/dashboards/infrastructure/templatetags/chart_helpers.py
@@ -0,0 +1,40 @@
+from django import template
+from django.utils import simplejson
+
+register = template.Library()
+
+
+@register.filter()
+def remaining_capacity_by_flavors(obj):
+ flavors = obj.list_flavors
+
+ decorated_obj = " ".join(
+ [("<p><strong>{0}</strong> {1}</p>").format(
+ str(flavor.used_instances),
+ flavor.name)
+ for flavor in flavors])
+
+ decorated_obj = ("<p>Capacity remaining by flavors: </p>" +
+ decorated_obj)
+
+ return decorated_obj
+
+
+@register.filter()
+def all_used_instances(obj):
+ flavors = obj.list_flavors
+
+ all_used_instances_info = []
+ for flavor in flavors:
+ info = {}
+ info['popup_used'] = (
+ '<p> {0}% total,'
+ ' <strong> {1} instances</strong> of {2}</p>'.format(
+ flavor.used_instances,
+ flavor.used_instances,
+ flavor.name))
+ info['used_instances'] = str(flavor.used_instances)
+
+ all_used_instances_info.append(info)
+
+ return simplejson.dumps(all_used_instances_info)