From 2a4d36a1b7534b03132d55f5d28a42e27cd00ecb Mon Sep 17 00:00:00 2001 From: Joe Rafaniello Date: Tue, 25 Feb 2020 13:41:34 -0500 Subject: [PATCH 1/4] Initial commit from ui-classic. Related to: https://github.com/ManageIQ/manageiq/issues/19674 (missing ReportFormatter for backend workers) https://github.com/ManageIQ/manageiq/issues/19863 (move code from UI-classic that can be called from either the frontend or backend) Transferred from ManageIQ/manageiq-ui-classic@24fc2bea4aed7a32543fad5b6602da2f363a60af --- lib/manageiq/reporting/charting.rb | 36 ++ .../reporting/charting/c3_charting.rb | 92 ++++ lib/manageiq/reporting/formatter.rb | 15 + lib/manageiq/reporting/formatter/c3.rb | 183 +++++++ lib/manageiq/reporting/formatter/c3_series.rb | 27 + .../reporting/formatter/chart_common.rb | 470 ++++++++++++++++++ lib/manageiq/reporting/formatter/converter.rb | 19 + lib/manageiq/reporting/formatter/html.rb | 65 +++ .../reporting/formatter/report_renderer.rb | 7 + lib/manageiq/reporting/formatter/text.rb | 262 ++++++++++ lib/manageiq/reporting/formatter/timeline.rb | 181 +++++++ .../reporting/formatter/timeline_message.rb | 150 ++++++ .../report_formater/c3_formatter_spec.rb | 164 ++++++ .../report_formater/chart_common_spec.rb | 55 ++ .../report_formater/text_formatter_spec.rb | 78 +++ .../report_formater/timeline_message_spec.rb | 17 + .../manageiq/report_formater/timeline_spec.rb | 285 +++++++++++ spec/support/report_helper.rb | 324 ++++++++++++ 18 files changed, 2430 insertions(+) create mode 100644 lib/manageiq/reporting/charting.rb create mode 100644 lib/manageiq/reporting/charting/c3_charting.rb create mode 100644 lib/manageiq/reporting/formatter.rb create mode 100644 lib/manageiq/reporting/formatter/c3.rb create mode 100644 lib/manageiq/reporting/formatter/c3_series.rb create mode 100644 lib/manageiq/reporting/formatter/chart_common.rb create mode 100644 lib/manageiq/reporting/formatter/converter.rb create mode 100644 lib/manageiq/reporting/formatter/html.rb create mode 100644 lib/manageiq/reporting/formatter/report_renderer.rb create mode 100644 lib/manageiq/reporting/formatter/text.rb create mode 100644 lib/manageiq/reporting/formatter/timeline.rb create mode 100644 lib/manageiq/reporting/formatter/timeline_message.rb create mode 100644 spec/lib/manageiq/report_formater/c3_formatter_spec.rb create mode 100644 spec/lib/manageiq/report_formater/chart_common_spec.rb create mode 100644 spec/lib/manageiq/report_formater/text_formatter_spec.rb create mode 100644 spec/lib/manageiq/report_formater/timeline_message_spec.rb create mode 100644 spec/lib/manageiq/report_formater/timeline_spec.rb create mode 100644 spec/support/report_helper.rb diff --git a/lib/manageiq/reporting/charting.rb b/lib/manageiq/reporting/charting.rb new file mode 100644 index 00000000000..f00ab747f7a --- /dev/null +++ b/lib/manageiq/reporting/charting.rb @@ -0,0 +1,36 @@ +class Charting + class << self + extend Forwardable + delegate [ + :backend, # charting backend name; FIXME: remove this method + :render_format, + :format, # format for Ruport renderer + :load_helpers, + :data_ok?, + :sample_chart, + :chart_names_for_select, + :chart_themes_for_select, + :serialized, + :deserialized, + :js_load_statement # javascript statement to reload charts + ] => :instance + end + + # discovery + # + # + def self.instance + @instance ||= new + end + + def self.new + self == Charting ? detect_available_plugin.new : super + end + + def self.detect_available_plugin + subclasses.select(&:available?).max_by(&:priority) + end +end + +# load all plugins +Dir.glob(File.join(File.dirname(__FILE__), "charting/*.rb")).each { |f| require_dependency f } diff --git a/lib/manageiq/reporting/charting/c3_charting.rb b/lib/manageiq/reporting/charting/c3_charting.rb new file mode 100644 index 00000000000..cb790cf759c --- /dev/null +++ b/lib/manageiq/reporting/charting/c3_charting.rb @@ -0,0 +1,92 @@ +class C3Charting < Charting + # for Charting.detect_available_plugin + def self.available? + true + end + + # for Charting.detect_available_plugin + def self.priority + 1000 + end + + # backend identifier + def backend + :c3 + end + + # format for rails' render + def render_format + :json + end + + # formatter for Rupport::Controller#render - see lib/report_formatter/... + def format + :c3 + end + + # called from each ApplicationController instance + def load_helpers(klass) + klass.instance_eval do + helper C3Helper + end + end + + def data_ok?(data) + obj = YAML.load(data) + !!obj && obj.kind_of?(Hash) && !obj[:options] + rescue Psych::SyntaxError, ArgumentError + false + end + + def sample_chart(_options, _report_theme) + sample = { + :data => { + :axis => {}, + :tooltip => {}, + :columns => [ + ['data1', 30, 200, 100, 400, 150, 250], + ['data2', 50, 20, 10, 40, 15, 25], + ['data3', 10, 25, 10, 250, 10, 30] + ], + }, + :miqChart => _options[:graph_type], + :miq => { :zoomed => false } + } + sample[:data][:groups] = [['data1','data2', 'data3']] if _options[:graph_type].include? 'Stacked' + sample + end + + def js_load_statement(delayed = false) + delayed ? 'setTimeout(function(){ load_c3_charts(); }, 100);' : 'load_c3_charts();' + end + + # list of available chart types - in options_for_select format + def chart_names_for_select + CHART_NAMES + end + + # list of themes - in options_for_select format + def chart_themes_for_select + [%w(Default default)] + end + + def serialized(data) + data.try(:to_yaml) + end + + def deserialized(data) + YAML.load(data) + end + + CHART_NAMES = [ + ["Bars (2D)", "Bar"], + ["Bars, Stacked (2D)", "StackedBar"], + ["Columns (2D)", "Column"], + ["Columns, Stacked (2D)", "StackedColumn"], + ["Donut (2D)", "Donut"], + ["Pie (2D)", "Pie"], + ["Line (2D)", "Line"], + ["Area (2D)", "Area"], + ["Area, Stacked (2D)", "StackedArea"], + ] +end diff --git a/lib/manageiq/reporting/formatter.rb b/lib/manageiq/reporting/formatter.rb new file mode 100644 index 00000000000..9398e329cab --- /dev/null +++ b/lib/manageiq/reporting/formatter.rb @@ -0,0 +1,15 @@ +include ActionView::Helpers::NumberHelper + +require 'report_formatter/report_renderer' +require 'report_formatter/c3' +require 'report_formatter/converter' +require 'report_formatter/html' +require 'report_formatter/text' +require 'report_formatter/timeline' + +module ReportFormatter + BLANK_VALUE = "Unknown" # Chart constant for nil or blank key values + CRLF = "\r\n" + LEGEND_LENGTH = 11 # Top legend text limit + LABEL_LENGTH = 21 # Chart label text limit +end diff --git a/lib/manageiq/reporting/formatter/c3.rb b/lib/manageiq/reporting/formatter/c3.rb new file mode 100644 index 00000000000..1530a42b51b --- /dev/null +++ b/lib/manageiq/reporting/formatter/c3.rb @@ -0,0 +1,183 @@ +module ReportFormatter + class C3Formatter < Ruport::Formatter + include ActionView::Helpers::UrlHelper + include ChartCommon + include MiqReport::Formatting + renders :c3, :for => ReportRenderer + + # series handling methods + def series_class + C3Series + end + + CONVERT_TYPES = { + "ColumnThreed" => "Column", + "ParallelThreedColumn" => "Column", + "StackedThreedColumn" => "StackedColumn", + "PieThreed" => "Pie", + "AreaThreed" => "Area", + "StackedAreaThreed" => "StackedArea" + } + def add_series(label, data) + @counter ||= 0 + @counter += 1 + series_id = @counter.to_s + limit = pie_type? ? LEGEND_LENGTH : LABEL_LENGTH + + if chart_is_2d? + mri.chart[:data][:columns] << [series_id, *data.map { |a| a[:value] }] + mri.chart[:data][:names][series_id] = slice_legend(_(label), limit) + mri.chart[:miq][:name_table][series_id] = label + else + data.each_with_index do |a, index| + id = index.to_s + mri.chart[:data][:columns].push([id, a[:value]]) + mri.chart[:data][:names][id] = slice_legend(_(a[:tooltip]), limit) + mri.chart[:miq][:name_table][id] = a[:tooltip] + end + end + + if chart_is_stacked? + mri.chart[:data][:groups][0] << series_id + end + end + + def add_axis_category_text(categories) + if chart_is_2d? + category_labels = categories.collect { |c| c.kind_of?(Array) ? c.first : c } + limit = pie_type? ? LEGEND_LENGTH : LABEL_LENGTH + mri.chart[:axis][:x][:categories] = category_labels.collect { |c| slice_legend(c, limit) } + mri.chart[:miq][:category_table] = category_labels + end + end + + # report building methods + def build_document_header + super + type = c3_convert_type(mri.graph[:type].to_s) + mri.chart = { + :miqChart => type, + :data => {:columns => [], :names => {}, :empty => {:label => {:text => _('No data available.')}}}, + :axis => {:x => {:tick => {}}, :y => {:tick => {}, :padding => {:bottom => 0}}}, + :tooltip => {:format => {}}, + :miq => {:name_table => {}, :category_table => {}}, + :legend => {} + } + + if chart_is_2d? + mri.chart[:axis][:x] = { + :categories => [], + :tick => {} + } + end + + if chart_is_stacked? + mri.chart[:data][:groups] = [[]] + end + + # chart is numeric + if mri.graph[:mode] == 'values' + custom_format = Array(mri[:col_formats])[Array(mri[:col_order]).index(raw_column_name)] + format, options = javascript_format(mri.graph[:column].split(/(? format, :options => options} + mri.chart[:axis][:y] = {:tick => {:format => axis_formatter}} + end + end + + # C&U chart + if graph_options[:chart_type] == :performance + unless mri.graph[:type] == 'Donut' || mri.graph[:type] == 'Pie' + mri.chart[:legend] = {:position => 'bottom'} + end + + return if mri.graph[:columns].blank? + column = grouped_by_tag_category? ? mri.graph[:columns][0].split(/_+/)[0..-2].join('_') : mri.graph[:columns][0] + format, options = javascript_format(column, nil) + return unless format + + axis_formatter = {:function => format, :options => options} + mri.chart[:axis][:y][:tick] = {:format => axis_formatter} + mri.chart[:miq][:format] = axis_formatter + end + end + + def c3_convert_type(type) + CONVERT_TYPES[type] || type + end + + def chart_is_2d? + ['Bar', 'Column', 'StackedBar', 'StackedColumn', 'Line', 'Area', 'StackedArea'].include?(c3_convert_type(mri.graph[:type])) + end + + def chart_is_stacked? + %w(StackedBar StackedColumn StackedArea).include?(mri.graph[:type]) + end + + # change structure of chart JSON to performance chart with timeseries data + def build_performance_chart_area(maxcols) + super + change_structure_to_timeseries + end + + def no_records_found_chart(*) + mri.chart = { + :axis => {:y => {:show => false}}, + :data => {:columns => [], :empty => {:label => {:text => _('No data available.')}}}, + :miq => {:empty => true}, + } + end + + def finalize_document + mri.chart + end + + private + + # change structure of hash from standard chart to timeseries chart + def change_structure_to_timeseries + # add 'x' as first element and move mri.chart[:axis][:x][:categories] to mri.chart[:data][:columns] as first column + x = mri.chart[:axis][:x][:categories] + x.unshift('x') + mri.chart[:data][:columns].unshift(x) + mri.chart[:data][:x] = 'x' + # set x axis type to timeseries and remove categories + mri.chart[:axis][:x] = {:type => 'timeseries', :tick => {}} + # set flag for performance chart + mri.chart[:miq][:performance_chart] = true + # this conditions are taken from build_performance_chart_area method from chart_commons.rb + if mri.db.include?("Daily") || (mri.where_clause && mri.where_clause.include?("daily")) + # set format for parsing + mri.chart[:data][:xFormat] = '%m/%d' + # set format for labels + mri.chart[:axis][:x][:tick][:format] = '%m/%d' + elsif mri.extras[:realtime] == true + mri.chart[:data][:xFormat] = '%H:%M:%S' + mri.chart[:axis][:x][:tick][:format] = '%H:%M:%S' + else + mri.chart[:data][:xFormat] = '%H:%M' + mri.chart[:axis][:x][:tick][:format] = '%H:%M' + end + end + + def build_reporting_chart(_maxcols) + mri.chart[:miq][:expand_tooltip] = true + super + end + + def build_reporting_chart_numeric(_maxcols) + mri.chart[:miq][:expand_tooltip] = true + super + end + + def build_performance_chart_pie(_maxcols) + mri.chart[:miq][:expand_tooltip] = true + super + end + + def grouped_by_tag_category? + !!(mri.performance && mri.performance.fetch_path(:group_by_category)) + end + end +end diff --git a/lib/manageiq/reporting/formatter/c3_series.rb b/lib/manageiq/reporting/formatter/c3_series.rb new file mode 100644 index 00000000000..a2f992630bc --- /dev/null +++ b/lib/manageiq/reporting/formatter/c3_series.rb @@ -0,0 +1,27 @@ +module ReportFormatter + class C3Series < Array + def initialize(*) + super() + end + + def push(datum) + super(datum) + end + + def sum + super { |datum| datum[:value].to_f } + end + + def value_at(index) + self[index][:value] + end + + def add_to_value(index, addition) + self[index][:value] += addition + end + + def set_to_zero(index) + self[index][:value] = 0 + end + end +end diff --git a/lib/manageiq/reporting/formatter/chart_common.rb b/lib/manageiq/reporting/formatter/chart_common.rb new file mode 100644 index 00000000000..3a6ea72b239 --- /dev/null +++ b/lib/manageiq/reporting/formatter/chart_common.rb @@ -0,0 +1,470 @@ +module ReportFormatter + module ChartCommon + def slice_legend(string, limit = LEGEND_LENGTH) + string.to_s.gsub(/\n/, ' ').truncate(limit) + end + + def nonblank_or_default(value) + value.blank? ? BLANK_VALUE : value.to_s + end + + delegate :mri, :to => :options + + def build_document_header + raise "Can't create a graph without a sortby column" if mri.sortby.nil? && + mri.db != "MiqReport" # MiqReport based charts are already sorted + raise "Graph type not specified" if mri.graph.nil? || + (mri.graph.kind_of?(Hash) && mri.graph[:type].nil?) + end + + delegate :graph_options, :to => :options + + def build_document_body + return no_records_found_chart if mri.table.nil? || mri.table.data.blank? + maxcols = 8 + fun = case graph_options[:chart_type] + when :performance then :build_performance_chart # performance chart (time based) + when :util_ts then :build_util_ts_chart # utilization timestamp chart (grouped columns) + else # reporting charts + mri.graph[:mode] == 'values' ? :build_reporting_chart_numeric : :build_reporting_chart + end + method(fun).call(maxcols) + end + + def build_document_footer + end + + protected + + # C&U performance charts (Cluster, Host, VM based) + def build_performance_chart_area(maxcols) + tz = mri.get_time_zone(Time.zone.name) + + mri.graph[:columns].each_with_index do |col, col_idx| + + next if col_idx >= maxcols + allnil = true + tip = graph_options[:trendtip] if col.starts_with?("trend") && graph_options[:trendtip] + categories = [] # Store categories and series counts in an array of arrays + series = series_class.new + mri.table.data.each_with_index do |r, d_idx| + rec_time = r["timestamp"].in_time_zone(tz) + + if mri.db.include?("Daily") || (mri.where_clause && mri.where_clause.include?("daily")) + categories.push(rec_time.month.to_s + "/" + rec_time.day.to_s) + elsif mri.extras[:realtime] == true + categories.push(rec_time.strftime("%H:%M:%S")) + else + categories.push(rec_time.hour.to_s + ":00") + end + val = r[col] + + if d_idx == mri.table.data.length - 1 && !tip.nil? + series.push(:value => val, :tooltip => tip) + else + series.push(:value => val) + end + allnil = false if !val.nil? + end + series.set_to_zero(-1) if allnil # XML/SWF Charts can't handle all nils, set the last value to 0 + add_axis_category_text(categories) + + head = mri.graph[:legends] ? mri.graph[:legends][col_idx] : mri.headers[mri.col_order.index(col)] # Use legend overrides, if present + + add_series(head, series) + end + end + + def rounded_value(value) + return 0 if value.blank? + value.round(graph_options[:decimals] || 0) + end + + def build_performance_chart_pie(_maxcols) + col = mri.graph[:columns].first + mri.table.sort_rows_by!(col, :order => :descending) + categories = [] # Store categories and series counts in an array of arrays + series = series_class.new + cat_cnt = 0 + cat_total = mri.table.size + mri.table.data.each do |r| + cat = cat_cnt > 6 ? 'Others' : r["resource_name"] + val = rounded_value(r[col]) + next if val == 0 + if cat.starts_with?("Others") && categories[-1].starts_with?("Others") # Are we past the top 10? + categories[-1] = "Others" + series.add_to_value(-1, val) # Accumulate the series value + next + end + categories.push(cat) + cat_cnt += 1 + series.push(:value => val) + end + + return no_records_found_chart if series.empty? + + add_axis_category_text(categories) + series.zip(categories) { |ser, category| ser[:tooltip] = category } + add_series('', series) + end + + def format_bytes_human_size_1 + { + :function => { + :name => 'bytes_to_human_size', + :precision => 1 + } + } + end + + # Utilization timestamp charts + def build_util_ts_chart_column + categories = [] # Store categories and series counts in an array of arrays + series = [] + mri.graph[:columns].each_with_index do |col, col_idx| + mri.table.data.each do |r| + if col_idx == 0 # First column is the category text + categories.push(r[col]) + else + series[col_idx - 1] ||= {} + series[col_idx - 1][:header] ||= mri.headers[mri.col_order.index(col)] # Add the series header + series[col_idx - 1][:data] ||= series_class.new + tip_key = col + '_tip' + tip = case r[0] # Override the formatting for certain column groups on single day percent utilization chart + when "CPU" + mri.format(tip_key, r[tip_key], :format => { + :function => { + :name => "mhz_to_human_size", + :precision => "1" + }}) + when "Memory" + mri.format(tip_key, r[tip_key].to_f * 1024 * 1024, :format => format_bytes_human_size_1) + when "Disk" + mri.format(tip_key, r[tip_key], :format => format_bytes_human_size_1) + else + mri.format(tip_key, r[tip_key]) + end + series[col_idx - 1][:data].push( + :value => mri.format(col, r[col]).to_f, # ?? .to_f ?? + :tooltip => tip + ) + end + end + end + + # Remove categories (and associated series values) that have all zero or nil values + (categories.length - 1).downto(0) do |i| + sum = series.reduce(0.0) { |a, e| a + e[:data].value_at(i).to_f } + next if sum != 0 + + categories.delete_at(i) + series.each { |s| s[:data].delete_at(i) } # Remove the data for this cat across all series + end + + # Remove any series where all values are zero or nil + series.delete_if { |s| s[:data].sum == 0 } + + if categories.empty? + no_records_found_chart("No data found for the selected day") + false + else + add_axis_category_text(categories) + series.each { |s| add_series(s[:header], s[:data]) } + true + end + end + + def keep_and_show_other + # Show other sum value by default + mri.graph.kind_of?(Hash) ? [mri.graph[:count].to_i, mri.graph[:other]] : [ReportController::Reports::Editor::GRAPH_MAX_COUNT, true] + end + + def build_reporting_chart_dim2 + (sort1, sort2) = mri.sortby + save1 = save2 = counter = save1_nonblank = save2_nonblank = nil + counts = {} # hash of hashes of counts + mri.table.data.each_with_index do |r, d_idx| + if d_idx == 0 || save1 != r[sort1].to_s + counts[save1_nonblank][save2_nonblank] = counter unless d_idx == 0 + save1 = r[sort1].to_s + save2 = r[sort2].to_s + save1_nonblank = nonblank_or_default(save1) + save2_nonblank = nonblank_or_default(save2) + counts[save1_nonblank] = Hash.new(0) + counter = 0 + else + if save2 != r[sort2].to_s # only the second sort field changed, save the count + counts[save1_nonblank][save2_nonblank] = counter + save2 = r[sort2].to_s + save2_nonblank = nonblank_or_default(save2) + counter = 0 + end + end + counter += 1 + end + # add the last key/value to the counts hash + counts[save1_nonblank][save2_nonblank] = counter + # We have all the counts, now we need to collect all of the . . . + sort1_vals = [] # sort field 1 values into an array and . . . + sort2_vals_counts = Hash.new(0) # sort field 2 values and counts into a Hash + counts.each do |key1, hash1| + sort1_vals.push(key1) + hash1.each { |key2, count2| sort2_vals_counts[key2] += count2 } + end + sort2_vals = sort2_vals_counts.sort { |a, b| b[1] <=> a[1] } # Sort the field values by count size descending + + # trim and add axis_category_text to the chart + sort1_vals.collect! { |value| slice_legend(value, LABEL_LENGTH) } + add_axis_category_text(sort1_vals) + + # Now go through the counts hash again and put out a series for each sort field 1 hash of counts + (keep, show_other) = keep_and_show_other + + # If there are more than keep categories Keep the highest counts + other = keep < sort2_vals.length ? sort2_vals.slice!(keep..-1) : nil + + sort2_vals.each do |val2| + series = counts.each_with_object(series_class.new) do |(key1, hash1), a| + a.push(:value => hash1[val2[0]], + :tooltip => "#{key1} / #{val2[0]}: #{hash1[val2[0]]}") + end + val2[0] = val2[0].to_s.gsub(/\\/, ' \ ') + add_series(val2[0].to_s, series) + end + + if other.present? && show_other # Sum up the other sort2 counts by sort1 value + series = series_class.new + counts.each do |key1, hash1| # Go thru each sort1 key and hash count + # Add in all of the remaining sort2 key counts + ocount = other.reduce(0) { |a, e| a + hash1[e[0]] } + series.push(:value => ocount, + :tooltip => "#{key1} / Other: #{ocount}") + end + add_series(_("Other"), series) + end + counts + end + + def extract_column_names + # examples: + # 'Vm.hardware-cpu_sockets' gives 'hardware-cpu_sockets' + # 'Host-v_total_vms' gives 'v_total_vms' + # 'Vm-num_cpu:total' gives 'num_cpu' and 'num_cpu__total' + # "Vm::Providers::InfraManager::Vm-num_cpu:total" + # gives 'Vm::Providers::InfraManager::Vm' and 'num_cpu__total' + stage1, aggreg = mri.graph[:column].split(/(? row[data_column_name], + :tooltip => tooltip) + categories.push([tooltip, row[data_column_name]]) + end + + if show_other + other_sum = Array(sorted_data[0, sorted_data.length - keep]) + .inject(0) { |sum, row| sum + (row[data_column_name] || 0) } + series.push(:value => other_sum, :tooltip => _('Other')) + categories.push([_('Other'), other_sum]) + end + + # Pie charts put categories in legend, else in axis labels + add_axis_category_text(categories) + + add_series(chart_is_2d? ? mri.chart_header_column : nil, series) + end + + def build_numeric_chart_grouped + (keep, show_other) = keep_and_show_other + show_other &&= (aggreg == :total) # FIXME: we only support :total + + groups = mri.build_subtotals.reject { |k, _| k == :_total_ } + sorted_data = groups.sort_by { |_, data| data[aggreg][raw_column_name] || 0 } + + categories = [] + series = sorted_data.reverse.take(keep) + .each_with_object(series_class.new(pie_type? ? :pie : :flat)) do |(key, data), a| + tooltip = key + tooltip = _('no value') if key.blank? + a.push(:value => data[aggreg][raw_column_name], + :tooltip => tooltip) + categories.push([tooltip, data[aggreg][raw_column_name]]) + end + + if show_other + other_sum = Array(sorted_data[0, sorted_data.length - keep]) + .inject(0) { |sum, (_key, row)| sum + row[aggreg][raw_column_name] } + + series.push(:value => other_sum, :tooltip => _('Other')) + categories.push([_('Other'), other_sum]) + end + + # Pie charts put categories in legend, else in axis labels + add_axis_category_text(categories) + + add_series(chart_is_2d? ? mri.chart_header_column : nil, series) + end + + def build_numeric_chart_grouped_2dim + (sort1, sort2) = mri.sortby + (keep, show_other) = keep_and_show_other + show_other &&= (aggreg == :total) # FIXME: we only support :total + + subtotals = mri.build_subtotals(true).reject { |k, _| k == :_total_ } + + # Group values by sort1 + # 3rd dimension in the chart is defined by sort2 + groups = mri.table.data.group_by { |row| row[sort1] } + + def_range_key2 = subtotals.keys.map { |key| key.split('__')[1] || '' }.sort.uniq + + group_sums = groups.keys.each_with_object({}) do |key1, h| + h[key1] = def_range_key2.inject(0) do |sum, key2| + sub_key = "#{key1}__#{key2}" + subtotals.key?(sub_key) ? sum + subtotals[sub_key][aggreg][raw_column_name] : sum + end + end + + sorted_sums = group_sums.sort_by { |_key, sum| sum } + + selected_groups = sorted_sums.reverse.take(keep) + + cathegory_texts = selected_groups.collect do |key, _| + label = key + label = _('no value') if label.blank? + label + end + cathegory_texts << _('Other') if show_other + + add_axis_category_text(cathegory_texts) + + if show_other + other_groups = Array(sorted_sums[0, sorted_sums.length - keep]) + other = other_groups.each_with_object(Hash.new(0)) do |(key, _), o| + groups[key].each { |row| o[row[sort2]] += row[raw_column_name] } + end + end + + # For each value in sort2 column we create a series. + sort2_values = mri.table.data.map { |row| row[sort2] }.uniq + sort2_values.each do |val2| + series = selected_groups.each_with_object(series_class.new) do |(key1, _), a| + sub_key = "#{key1}__#{val2}" + value = subtotals.key?(sub_key) ? subtotals[sub_key][aggreg][raw_column_name] : 0 + + a.push(:value => value, + :tooltip => "#{key1} / #{val2}: #{value}") + end + + series.push(:value => other[val2], + :tooltip => "Other / #{val2}: #{other[val2]}") if show_other + label = val2 if val2.kind_of?(String) + label = label.to_s.gsub(/\\/, ' \ ') + label = _('no value') if label.blank? + add_series(label, series) + end + groups.keys.collect { |k| k.blank? ? _('no value') : k } + end + + def pie_type? + @pie_type ||= mri.graph[:type] =~ /^(Pie|Donut)/ + end + + def build_reporting_chart_other + save_key = nil + counter = 0 + categories = [] # Store categories and series counts in an array of arrays + mri.table.data.each_with_index do |r, d_idx| + category_changed = save_key != r[mri.sortby[0]] + not_first_iteration = d_idx > 0 + if not_first_iteration && category_changed + categories.push([save_key, counter]) # Push current category and count onto the array + counter = 0 + end + save_key = r[mri.sortby[0]] + counter += 1 + end + categories.push([save_key, counter]) # Push last category and count onto the array + + (keep, show_other) = keep_and_show_other + kept_categories = categories + kept_categories.reject! { |a| a.first.nil? } + kept_categories = kept_categories.sort_by(&:first).take(keep) + kept_categories.reverse! if mri.order == "Descending" + kept_categories.push(["Other", (categories - kept_categories).reduce(0) { |a, e| a + e.last }]) if show_other + kept_categories.map { |cat| [nonblank_or_default(cat.first), cat.last] } + + series = kept_categories.each_with_object( + series_class.new(pie_type? ? :pie : :flat)) do |cat, a| + a.push(:value => cat.last, :tooltip => "#{cat.first}: #{cat.last}") + end + + # Pie charts put categories in legend, else in axis labels + add_axis_category_text(kept_categories) + add_series(chart_is_2d? ? mri.chart_header_column : nil, series) + end + + # C&U performance charts (Cluster, Host, VM based) + def build_performance_chart(maxcols) + case mri.graph[:type] + when "Area", "AreaThreed", "Line", "StackedArea", + "StackedThreedArea", "ParallelThreedColumn" + build_performance_chart_area(maxcols) + when "Pie", "PieThreed" + build_performance_chart_pie(maxcols) + end + end + + # Utilization timestamp charts + def build_util_ts_chart(_maxcols) + build_util_ts_chart_column if %w(Column ColumnThreed).index(mri.graph[:type]) + end + + def build_reporting_chart_numeric(_maxcols) + return no_records_found_chart(_('Invalid chart definition')) unless mri.graph[:column].present? + if mri.group.nil? + build_numeric_chart_simple + else + mri.dims == 2 ? build_numeric_chart_grouped_2dim : build_numeric_chart_grouped + end + end + + def build_reporting_chart(_maxcols) + mri.dims == 2 ? build_reporting_chart_dim2 : build_reporting_chart_other + end + end +end diff --git a/lib/manageiq/reporting/formatter/converter.rb b/lib/manageiq/reporting/formatter/converter.rb new file mode 100644 index 00000000000..dcac8fed6e1 --- /dev/null +++ b/lib/manageiq/reporting/formatter/converter.rb @@ -0,0 +1,19 @@ +module ReportFormatter + class Converter + # generate a ruport table from an array of hashes where the keys are the column names + def self.hashes2table(hashes, options) + return Ruport::Data::Table.new if hashes.blank? + + data = hashes.inject([]) do |arr, h| + nh = {} + options[:only].each { |col| nh[col] = h[col] } + arr << nh + end + + data = data[0..options[:limit] - 1] if options[:limit] # apply limit + Ruport::Data::Table.new(:data => data, + :column_names => options[:only], + :filters => options[:filters]) + end + end +end diff --git a/lib/manageiq/reporting/formatter/html.rb b/lib/manageiq/reporting/formatter/html.rb new file mode 100644 index 00000000000..07f10a1a88d --- /dev/null +++ b/lib/manageiq/reporting/formatter/html.rb @@ -0,0 +1,65 @@ +module ReportFormatter + class ReportHTML < Ruport::Formatter + renders :html, :for => ReportRenderer + + def build_html_title + mri = options.mri + mri.html_title = '' + mri.html_title << "
" + mri.html_title << "" + mri.html_title << '
' + mri.html_title << '
' + end + + def pad(str, len) + return "".ljust(len) if str.nil? + str = str.slice(0, len) # truncate long strings + str.ljust(len) # pad with whitespace + end + + def build_document_header + build_html_title + end + + def build_document_body + mri = options.mri + output << "" + output << "" + output << "" + + # table heading + unless mri.headers.nil? + mri.headers.each do |h| + output << "" + end + output << "" + output << "" + end + output << '' + output << mri.build_html_rows.join + output << '' + end + + def build_document_footer + mri = options.mri + output << "" + output << "" + output << "" + output << "
" << CGI.escapeHTML(h.to_s) << "
" + output << "" + output << "" + output << "
" + + if mri.filter_summary + output << mri.filter_summary.to_s + end + end + + def finalize_document + output + end + end +end diff --git a/lib/manageiq/reporting/formatter/report_renderer.rb b/lib/manageiq/reporting/formatter/report_renderer.rb new file mode 100644 index 00000000000..3964532d340 --- /dev/null +++ b/lib/manageiq/reporting/formatter/report_renderer.rb @@ -0,0 +1,7 @@ +module ReportFormatter + class ReportRenderer < Ruport::Controller + stage :document_header, :document_body, :document_footer + finalize :document + options { |o| o.mri = o.show_title = o.theme = o.table_width = o.alignment = o.graph_options = nil } + end +end diff --git a/lib/manageiq/reporting/formatter/text.rb b/lib/manageiq/reporting/formatter/text.rb new file mode 100644 index 00000000000..a8faaf6ec18 --- /dev/null +++ b/lib/manageiq/reporting/formatter/text.rb @@ -0,0 +1,262 @@ +module ReportFormatter + class ReportText < Ruport::Formatter::Text + renders :text, :for => ReportRenderer + + # determines the text widths for each column. + def calculate_max_col_widths + mri = options.mri + tz = mri.get_time_zone(Time.zone.name) + + @max_col_width = [] + unless mri.headers.empty? + mri.headers.each_index do |i| + @max_col_width[i] = mri.headers[i].to_s.length + end + end + mri.table.data.each do |r| + mri.col_formats ||= [] # Backward compat - create empty array for formats + mri.col_order.each_with_index do |f, i| + unless ["", ""].include?(mri.db) + data = mri.format(f, + r[f], + :format => mri.col_formats[i] ? mri.col_formats[i] : :_default_, + :tz => tz) + else + data = r[f].to_s + end + if !@max_col_width[i] || data.length > @max_col_width[i] + @max_col_width[i] = data.length + end + end + end + end + + # method to get friendly values for company tag and user filters + def calculate_filter_names(tag) + categories = Classification.categories.collect { |c| c if c.show }.compact + tag_val = "" + categories.each do |category| + entries = {} + category.entries.each do |entry| + entries[entry.description] = entry.tag.name # Get the fully qual tag name + if tag == entry.tag.name + tag_val = "#{category.description}: #{entry.description}" + end + end + end + tag_val + end + + # Uses the column names from the given Data::Table to generate a table + # header. + # + # calls fit_to_width to truncate table heading if necessary. + def build_document_header + mri = options.mri + raise "No settings configured for Table" if mri.table.nil? + calculate_max_col_widths + @hr = hr + + unless mri.title.nil? # generate title line, if present + output << fit_to_width(@hr) + + temp_title = mri.title + temp_title << " (" << mri.report_run_time.to_s << ")" unless mri.report_run_time.nil? + t = temp_title.center(@line_len - 2) + output << fit_to_width("|#{t}|" + CRLF) + if !mri.db.nil? && mri.db == "" + t2 = "(* = Value changed from previous column)" + t2 = t2.center(@line_len - 2) + output << fit_to_width("|#{t2}|" + CRLF) + end + end + + return if mri.headers.empty? + c = mri.headers.dup + # Remove headers of hidden columns + mri.col_order.each_with_index do |f, i| + c.delete_at(i) if mri.column_is_hidden?(f) + end + c.each_with_index do |f, i| + c[i] = f.to_s.center(@max_col_width[i]) + end + output << fit_to_width("#{@hr}| #{Array(c).join(' | ')} |" + CRLF) + end + + # Generates the body of the text table. + # + # Defaults to numeric values being right justified, and other values being + # left justified. Can be changed to support centering of output by + # setting alignment to :center + # + # Uses fit_to_width to truncate table if necessary + def build_document_body + mri = options.mri + tz = mri.get_time_zone(Time.zone.name) + s = @hr + + save_val = nil + counter = 0 + + row_limit = mri.rpt_options && mri.rpt_options[:row_limit] ? mri.rpt_options[:row_limit] : 0 + use_table = mri.sub_table ? mri.sub_table : mri.table + use_table.data.each_with_index do |r, d_idx| + break if row_limit != 0 && d_idx > row_limit - 1 + line = [] + line_wrapper = false # Clear line wrapper flag + if [""].include?(mri.db) && r[0] == "% Match:" + line_wrapper = true # Wrap compare % lines with header rows + elsif [""].include?(mri.db) && r[0] == "Changed:" + line_wrapper = true # Wrap drift changed lines with header rows + end + mri.col_formats ||= [] # Backward compat - create empty array for formats + mri.col_order.each_with_index do |f, i| + next if mri.column_is_hidden?(f) + + unless ["", ""].include?(mri.db) + data = mri.format(f, + r[f], + :format => mri.col_formats[i] ? mri.col_formats[i] : :_default_, + :tz => tz) + else + data = r[f].to_s + end + if options.alignment.eql? :center + line << data.center(@max_col_width[i]) + else + align = data.kind_of?(Numeric) ? :rjust : :ljust + line << data.send(align, @max_col_width[i]) + end + end + + # generate a break line if grouping is turned on + if ["y", "c"].include?(mri.group) && !mri.sortby.nil? + if d_idx > 0 && save_val != r[mri.sortby[0]] + if mri.group == "c" + s += @hr + t = " Total for #{save_val}: #{counter} ".center(@line_len - 2) + s += fit_to_width("|#{t}|" + CRLF) + s += @hr + counter = 0 + else + s += @hr + end + end + save_val = r[mri.sortby[0]] + counter += 1 + end + s += @hr if line_wrapper + s += "| #{line.join(' | ')} |" + CRLF + s += @hr if line_wrapper + end + + # see if a final group line needs to be written + if ["y", "c"].include?(mri.group) && !mri.sortby.nil? + if mri.group == "c" + s += @hr + t = " Total for #{save_val}: #{counter} ".center(@line_len - 2) + s += fit_to_width("|#{t}|" + CRLF) + end + end + + s += @hr + output << fit_to_width(s) + end + + def build_document_footer + mri = options.mri + tz = mri.get_time_zone(Time.zone.name) + if !mri.user_categories.blank? || !mri.categories.blank? || !mri.conditions.nil? || !mri.display_filter.nil? + output << fit_to_width(@hr) + unless mri.user_categories.blank? + user_filters = mri.user_categories.flatten + unless user_filters.blank? + customer_name = Tenant.root_tenant.name + user_filter = "User assigned " + customer_name + " Tag filters:" + t = user_filter.ljust(@line_len - 2) + output << fit_to_width("|#{t}|" + CRLF) + user_filters.each do |filters| + tag_val = " " + calculate_filter_names(filters) + tag_val1 = tag_val + " " * (@line_len - tag_val.length - 2) + output << fit_to_width("|#{tag_val1}|" + CRLF) + end + end + end + + unless mri.categories.blank? + categories = mri.categories.flatten + unless categories.blank? + customer_name = Tenant.root_tenant.name + customer_name_title = "Report based " + customer_name + " Tag filters:" + t = customer_name_title + " " * (@line_len - customer_name_title.length - 2) + output << fit_to_width("|#{t}|" + CRLF) + categories.each do |filters| + tag_val = " " + calculate_filter_names(filters) + tag_val1 = tag_val + " " * (@line_len - tag_val.length - 2) + output << fit_to_width("|#{tag_val1}|" + CRLF) + end + end + end + + unless mri.conditions.nil? + if mri.conditions.kind_of?(Hash) + filter_fields = "Report based filter fields:" + t = filter_fields.ljust(@line_len - 2) + output << fit_to_width("|#{t}|" + CRLF) + + # Clean up the conditions for display + tables = mri.conditions[:field].split("-")[0].split(".") # Get the model and tables + field = Dictionary.gettext(tables[0], :type => :model, :notfound => :titleize) # Start with the model + tables[1..-1].each do |t| # Add on any tables + field += "." + Dictionary.gettext(t, :type => :table, :notfound => :titleize) + end + # Add on the column name + field += " : " + Dictionary.gettext(mri.conditions[:field].split("-")[1], :type => :column, :notfound => :titleize) + + filter_val = " " + field + " " + mri.conditions[:operator] + " " + mri.conditions[:string].to_s + t = filter_val + " " * (@line_len - filter_val.length - 2) + output << fit_to_width("|#{t}|" + CRLF) + else + filter_fields = "Report based filter fields:" + t = filter_fields.ljust(@line_len - 2) + output << fit_to_width("|#{t}|" + CRLF) + filter_val = mri.conditions.to_human + t = filter_val.ljust(@line_len - 2) + output << fit_to_width("|#{t}|" + CRLF) + end + end + + unless mri.display_filter.nil? + filter_fields = "Display Filter:" + t = filter_fields.ljust(@line_len - 2) + output << fit_to_width("|#{t}|" + CRLF) + filter_val = mri.display_filter.to_human + t = filter_val + " " * (@line_len - filter_val.length - 2) + output << fit_to_width("|#{t}|" + CRLF) + end + end + + output << fit_to_width(@hr) + # Label footer with last run on time of selected report or current time for other downloads + last_run_on = mri.rpt_options && mri.rpt_options[:last_run_on] || Time.zone.now + cr = format_timezone(last_run_on, tz).to_s + f = cr.center(@line_len - 2) + output << fit_to_width("|#{f}|" + CRLF) + output << fit_to_width(@hr) + end + + # Generates the horizontal rule by calculating the total table width and + # then generating a bar that looks like this: + # + # "+------------------+" + def hr + columns = options.mri.table.column_names + if columns.include?("id") # Use 1 less column if "id" is present + @line_len = @max_col_width.inject((columns.length - 1) * 3) { |s, e| s + e } + 1 + else + @line_len = @max_col_width.inject(columns.length * 3) { |s, e| s + e } + end + "+" + "-" * (@line_len - 2) + "+" + CRLF + end + end +end diff --git a/lib/manageiq/reporting/formatter/timeline.rb b/lib/manageiq/reporting/formatter/timeline.rb new file mode 100644 index 00000000000..a25ba307dca --- /dev/null +++ b/lib/manageiq/reporting/formatter/timeline.rb @@ -0,0 +1,181 @@ +# Timeline formatter - creates Timeline XML stream to feed Simile Timelines +module ReportFormatter + class ReportTimeline < Ruport::Formatter + renders :timeline, :for => ReportRenderer + + # create the graph object and add titles, fonts, and colors + def build_document_header + mri = options.mri + raise "No settings configured for Timeline" if mri.timeline.nil? + end + + # Generates the body of the timeline + def build_document_body + mri = options.mri + tz = mri.get_time_zone(Time.zone.name) + # Calculate the earliest time of events to show + unless mri.timeline[:last_unit].nil? + # START of TIMELINE TIMEZONE Code + @start_time = format_timezone(Time.now, tz, 'raw') - mri.timeline[:last_time].to_i.send(mri.timeline[:last_unit].downcase) + # END of TIMELINE TIMEZONE Code + end + + mri.extras ||= {} # Create hash to store :tl_position setting + + @events = [] + @events_data = [] + tlfield = mri.timeline[:field].split("-") # Split the table and field + if tlfield.first.include?(".") # If table has a period (from a sub table) + col = tlfield.first.split(".").last + "." + tlfield.last # use subtable.field + else + col = tlfield.last # Not a subtable, just grab the field name + end + + # some of the OOTB reports have db as EventStream or PolicyEvent, + # those do not have event categories, so need to go thru else block for such reports. + if (mri.db == "EventStream" || mri.db == "PolicyEvent") && mri.rpt_options.try(:[], :categories) + event_map = mri.table.data.each_with_object({}) do |event, buckets| + bucket_name = mri.rpt_options[:categories].detect do |_, options| + options[:include_set].include?(event.event_type) + end&.last.try(:[], :display_name) + + bucket_name ||= mri.rpt_options[:categories].detect do |_, options| + options[:regexes].any? { |regex| regex.match(event.event_type) } + end.last[:display_name] + + buckets[bucket_name] ||= [] + buckets[bucket_name] << event + end + + event_map.each do |name, events| + @events_data = [] + events.each { |row| tl_event(row, col) } + @events.push(:name => name, :data => [@events_data]) + end + else + mri.table.data.each_with_index do |row, _d_idx| + tl_event(row, col) # Add this row to the tl event xml + end + @events.push(:data => [@events_data]) + end + # START of TIMELINE TIMEZONE Code + mri.extras[:tl_position] ||= format_timezone(Time.now, tz, 'raw') # If position not set, default to now + # END of TIMELINE TIMEZONE Code + output << @events.to_json + end + + def tl_event(row, col) + mri = options.mri + tz = mri.get_time_zone(Time.zone.name) + etime = row[col] + return if etime.nil? # Skip nil dates - Sprint 41 + return if !@start_time.nil? && etime < @start_time # Skip if before start time limit + # START of TIMELINE TIMEZONE Code + mri.extras[:tl_position] ||= format_timezone(etime.to_time, tz, 'raw') + if mri.timeline[:position] && mri.timeline[:position] == "First" + mri.extras[:tl_position] = format_timezone(etime.to_time, tz, 'raw') if format_timezone(etime.to_time, tz, 'raw') < format_timezone(mri.extras[:tl_position], tz, 'raw') + elsif mri.timeline[:position] && mri.timeline[:position] == "Current" + # if there is item with current time or greater then use that else, use right most one. + if format_timezone(etime.to_time, tz, 'raw') >= format_timezone(Time.now, tz, 'raw') && format_timezone(etime.to_time, tz, 'raw') <= format_timezone(mri.extras[:tl_position], tz, 'raw') + mri.extras[:tl_position] = format_timezone(etime.to_time, tz, 'raw') + else + mri.extras[:tl_position] = format_timezone(etime.to_time, tz, 'raw') if format_timezone(etime.to_time, tz, 'raw') > format_timezone(mri.extras[:tl_position], tz, 'raw') + end + else + mri.extras[:tl_position] = format_timezone(etime.to_time, tz, 'raw') if format_timezone(etime.to_time, tz, 'raw') > format_timezone(mri.extras[:tl_position], tz, 'raw') + end + # END of TIMELINE TIMEZONE Code + if row["id"] # Make sure id column is present + rec = mri.db.constantize.find_by_id(row['id']) + end + unless rec.nil? + case mri.db + when "Vm" + e_title = rec[:name] + when "Host" + e_title = rec[:name] + when "EventStream" + ems_cloud = false + if rec[:ems_id] && ExtManagementSystem.exists?(rec[:ems_id]) + ems = ExtManagementSystem.find(rec[:ems_id]) + ems_cloud = true if ems.kind_of?(EmsCloud) + ems_container = true if ems.kind_of?(::ManageIQ::Providers::ContainerManager) + end + if !ems_cloud + e_title = if rec[:vm_name] # Create the title using VM name + rec[:vm_name] + elsif rec[:host_name] # or Host Name + rec[:host_name] + elsif rec[:ems_cluster_name] # or Cluster Name + rec[:ems_cluster_name] + elsif rec[:container_name] + rec[:container_name] + elsif rec[:container_group_name] + rec[:container_group_name] + elsif rec[:container_replicator_name] + rec[:container_replicator_name] + elsif rec[:container_node_name] + rec[:container_node_name] + end + end + else + e_title = rec[:name] ? rec[:name] : row[mri.col_order.first].to_s + end + end + e_title ||= ems ? ems.name : "No VM, Host, or MS" + + # manipulating column order to display timestamp at the end of the bubble. + field = mri.timeline[:field].split("-") + if ems && ems_cloud + # Change labels to be cloud specific + vm_name_idx = mri.col_order.index("vm_name") + mri.headers[vm_name_idx] = "Source Instance" if vm_name_idx + vm_location_idx = mri.col_order.index("vm_location") + mri.headers[vm_location_idx] = "Source Instance Location" if vm_location_idx + dest_vm_name_idx = mri.col_order.index("dest_vm_name") + mri.headers[dest_vm_name_idx] = "Destination Instance" if dest_vm_name_idx + dest_vm_location_idx = mri.col_order.index("dest_vm_location") + mri.headers[dest_vm_location_idx] = "Destination Instance Location" if dest_vm_location_idx + else + mri.col_order.delete("availability_zone.name") + mri.headers.delete("Availability Zone") + end + col_order = copy_array(mri.col_order) + headers = copy_array(mri.headers) + i = col_order.rindex(field.last) + if i.nil? + # Adding a check incase timeline field came in with model/table in front of them + # i.e. PolicyEvent.miq_policy_sets-created_on + field_with_prefix = "#{field.first.split('.').last}.#{field.last}" + i = col_order.rindex(field_with_prefix) + col_order.delete(field_with_prefix) + col_order.push(field_with_prefix) + else + col_order.delete(field.last) + col_order.push(field.last) + end + j = headers[i] + headers.delete(j) + headers.push(j) + + flags = {:ems_cloud => ems_cloud, + :ems_container => ems_container, + :time_zone => tz} + tl_message = TimelineMessage.new(row, rec, flags, mri.db) + event_data = {} + col_order.each_with_index do |co, co_idx| + value = tl_message.message_html(co) + next if value.to_s.empty? || co == "id" + event_data[co] = { + :value => value, + :text => headers[co_idx] + } + end + + # Add the event to the timeline + @events_data.push("start" => format_timezone(row[col], tz, 'view'), + "title" => e_title.length < 20 ? e_title : e_title[0...17] + "...", + "event" => event_data) + end + end +end diff --git a/lib/manageiq/reporting/formatter/timeline_message.rb b/lib/manageiq/reporting/formatter/timeline_message.rb new file mode 100644 index 00000000000..565f644a32a --- /dev/null +++ b/lib/manageiq/reporting/formatter/timeline_message.rb @@ -0,0 +1,150 @@ +module ReportFormatter + class TimelineMessage + TIMELINE_TIME_COLUMNS = %w(created_on timestamp).freeze + + def initialize(row, event, flags, db) + @row, @event, @flags, @db = row, event, flags, db + end + + def message_html(column) + @column = column + field = column.tr('.', '_').to_sym + respond_to?(field, true) ? send(field).to_s : text + end + + private + + def vm_name + "#{text}" if @event.vm_or_template_id + end + + def src_vm_name + "#{text}" if @event.src_vm_or_template + end + + def dest_vm_name + "#{text}" if @event.dest_vm_or_template_id + end + + def host_name + "#{text}" if @event.host_id + end + + def dest_host_name + "#{text}" if @event.dest_host_id + end + + def target_name + e_text = if @event.target_name # Create the title using Policy description + @event.target_name + elsif @event.miq_policy_id && MiqPolicy.exists?(@event.miq_policy_id) # or Policy name + MiqPolicy.find(@event.miq_policy_id).name + else + _("Policy no longer exists") + end + unless @event.target_id.nil? + e_text += "
#{Dictionary.gettext(@event.target_class, :type => :model, :notfound => :titleize)}: " + e_text += "#{@event.target_name}" + end + assigned_profiles = @event.miq_policy_sets.each_with_object({}) do |profile, hsh| + hsh[profile.id] = profile.description unless profile.description.nil? + end + + unless @event.event_type.nil? + e_text += "
#{_("Assigned Profiles")}: " + assigned_profiles.each_with_index do |p, i| + e_text += "#{p[1]}" + e_text += ", " if assigned_profiles.length > 1 && i < assigned_profiles.length + end + end + e_text + end + + def ems_cluster_name + "#{text}" if @event.ems_cluster_id + end + + def availability_zone_name + if @event.availability_zone_id + "#{text}" + end + end + + def container_node_name + "#{text}" if @event.container_node_id + end + + def container_group_name + "#{text}" if @event.container_group_id + end + + def container_name + "#{text}" if @event.container_id + end + + def container_replicator_name + if @event.container_replicator_id + "#{text}" + end + end + + def middleware_name + mw_id_cols = EmsEvent.column_names.select { |n| n.match('middleware_.+_id') } + mw_id_col = mw_id_cols.find { |c| @event[c] } + unless mw_id_col.nil? + mw_type = mw_id_col.slice(0, mw_id_col.rindex('_id')) + mw_name_col = mw_type + '_name' + "#{@event[mw_name_col]}" + end + end + + def ext_management_system_name + if @event.ext_management_system && @event.ext_management_system.id + provider_id = @event.ext_management_system.id + if ems_cloud + # restful route is used for cloud provider unlike infrastructure provider + "#{text}" + elsif ems_container + "#{text}" + elsif ems_mw + "#{text}" + else + "#{text}" + end + end + end + + def resource_name + if @db == 'BottleneckEvent' + db = if ems_cloud && @event.resource_type == 'ExtManagementSystem' + 'ems_cloud' + elsif @event.resource_type == 'ExtManagementSystem' + 'ems_infra' + else + "#{@event.resource_type.underscore}/show" + end + "#{@event.resource_name}" + end + end + + def text + if @row[@column].kind_of?(Time) || TIMELINE_TIME_COLUMNS.include?(@column) + format_timezone(Time.parse(@row[@column].to_s).utc, @flags[:time_zone], "gtl") + else + @row[@column].to_s + end + end + + def ems_cloud + @flags[:ems_cloud] + end + + def ems_container + @flags[:ems_container] + end + + def ems_mw + @flags[:ems_mw] + end + end +end diff --git a/spec/lib/manageiq/report_formater/c3_formatter_spec.rb b/spec/lib/manageiq/report_formater/c3_formatter_spec.rb new file mode 100644 index 00000000000..09e85f1b879 --- /dev/null +++ b/spec/lib/manageiq/report_formater/c3_formatter_spec.rb @@ -0,0 +1,164 @@ +describe ReportFormatter::C3Formatter do + include Spec::Support::ReportHelper + + before do + allow(Charting).to receive(:backend).and_return(:c3) + allow(Charting).to receive(:format).and_return(:c3) + end + + describe "#add_series" do + it "does not raise error for 'stack' chart" do + report = numeric_chart_3d(true) + expect { render_report(report) }.to_not raise_error + end + end + + context '#build_numeric_chart_grouped' do + [true, false].each do |other| + it "builds 2d numeric charts from summaries #{other ? 'with' : 'without'} 'other'" do + report = numeric_charts_2d_from_summaries(other) + + expect_any_instance_of(described_class).to receive(:build_numeric_chart_grouped).once.and_call_original + render_report(report) + expect(report.chart[:data][:columns][0][1]).to eq(4.0) + expect(report.chart[:data][:columns][0][-1]).to eq(4) if other + end + end + end + + context '#build_numeric_chart_simple' do + let(:report) { numeric_chart_simple } + let(:long_report) { numeric_chart_simple_with_long_strings } + + it "report chart have right data in ascending order" do + report.col_formats = [nil, :general_number_precision_0] + render_report(report) + expect(report.chart[:data][:columns][0].count).to eq(report.table.data.count + 1) + expect(report.chart[:data][:columns][0][1]).to eq(2024) + end + + it "handles null data in chart column" do + report = null_data_chart + + expect_any_instance_of(described_class).to receive(:build_numeric_chart_simple).once.and_call_original + render_report(report) + end + + it "handle long strings" do + render_report(long_report) + expect(long_report.chart[:miq][:category_table][2]).to eq(long_category) + expect(long_report.chart[:miq][:name_table]['1']).to eq('RAM Size (MB)') + end + end + + context '#build_numeric_chart_simple' do + [true, false].each do |other| + it "builds 2d numeric charts #{other ? 'with' : 'without'} 'other'" do + report = numeric_chart_simple2(other) + + expect_any_instance_of(described_class).to receive(:build_numeric_chart_simple).once.and_call_original + render_report(report) + expect(report.chart[:data][:columns][0][1]).to eq(15) + expect(report.chart[:data][:columns][0][-1]).to eq(1) if other + end + end + + it "handles null data in chart column" do + report = null_data_chart + + expect_any_instance_of(described_class).to receive(:build_numeric_chart_simple).once.and_call_original + render_report(report) + end + end + + context '#build_numeric_chart_grouped_2dim' do + [true, false].each do |other| + it "builds 3d numeric charts #{other ? 'with' : 'without'} 'other'" do + report = numeric_chart_3d(other) + + expect_any_instance_of(described_class).to receive(:build_numeric_chart_grouped_2dim).once.and_call_original + render_report(report) + expect(report.chart[:data][:columns][0][1]).to eq(6_656) + expect(report.chart[:data][:columns][0][2]).to eq(4_096) + expect(report.chart[:data][:columns][1][1]).to eq(1_024) + expect(report.chart[:data][:columns][0][-1]).to eq(1_024) if other + labels = ["MTC-RHEVM-3.0", "openstack"] + labels.push("Other") if other + expect(report.chart[:axis][:x][:categories]).to eq(labels) + expect(report.chart[:miq][:category_table]).to eq(labels) + end + end + + it 'handles namespace-prefixed class names in chart column' do + report = chart_with_namespace_prefix + + expect_any_instance_of(described_class).to receive(:build_numeric_chart_grouped_2dim).once.and_call_original + render_report(report) + end + end + + context '#C&U charts without grouping' do + let(:report) { cu_chart_without_grouping } + before do + render_report(report, &proc { |e| e.options.graph_options = { :chart_type => :performance } }) + end + + it "has right data" do + expect(report.chart[:data][:columns][0].count).to eq(report.table.data.count + 1) + expect(report.chart[:data][:columns][0]).to eq(["x", "8/19", "8/20"]) + expect(report.chart[:data][:columns][1]).to eq(["1", 19_986.0, 205_632.0]) + expect(report.chart[:data][:columns][2]).to eq(["2", 41_584.0, 41_584.0]) + end + + it "has right type" do + expect(report.chart[:axis][:x][:type]).to eq("timeseries") + end + + it 'has right formatting functions' do + expect(report.chart[:axis][:y][:tick][:format][:function]).to eq("mhz_to_human_size") + expect(report.chart[:miq][:format][:function]).to eq("mhz_to_human_size") + end + it 'has right tabels' do + expect(report.chart[:miq][:name_table]).to eq("1" => "Avg Used", "2" => "Max Available") + expect(report.chart[:miq][:category_table]).to eq(["8/19", "8/20"]) + end + end + + context '#C&U charts with grouping' do + let(:report) { cu_chart_with_grouping } + before do + render_report(report, &proc { |e| e.options.graph_options = { :chart_type => :performance } }) + end + + it "has right data" do + expect(report.chart[:data][:columns][0].count).to eq(report.table.data.count + 1) + expect(report.chart[:data][:columns][0]).to eq(["x", "8/19", "8/20"]) + expect(report.chart[:data][:columns][1]).to eq(["1", 19_986.0, 205_632.0]) + expect(report.chart[:data][:columns][2]).to eq(["2", 41_584.0, 41_584.0]) + end + + it "has right type" do + expect(report.chart[:axis][:x][:type]).to eq("timeseries") + end + + it 'has right formatting functions' do + expect(report.chart[:axis][:y][:tick][:format][:function]).to eq("mhz_to_human_size") + expect(report.chart[:miq][:format][:function]).to eq("mhz_to_human_size") + end + it 'has right tabels' do + expect(report.chart[:miq][:name_table]).to eq("1" => "Avg Used", "2" => "Max Available") + expect(report.chart[:miq][:category_table]).to eq(["8/19", "8/20"]) + end + end + + context '#C&U charts with no data' do + let(:report) { cu_chart_with_grouping } + before do + render_report(report, &proc { |e| e.options.graph_options = { :chart_type => :performance } }) + end + + it "has right empty data description" do + expect(report.chart[:data][:empty][:label][:text]).to eq("No data available.") + end + end +end diff --git a/spec/lib/manageiq/report_formater/chart_common_spec.rb b/spec/lib/manageiq/report_formater/chart_common_spec.rb new file mode 100644 index 00000000000..584fbe8d569 --- /dev/null +++ b/spec/lib/manageiq/report_formater/chart_common_spec.rb @@ -0,0 +1,55 @@ +# describe ReportFormatter::ChartCommon do +# +# We have to operate on the specific class although we are testing the common behavior. +# Otherwise expect_any_instance_of(described_class).to receive(:build_performance_chart_area).once.and_call_original +# leads to with: +# SystemStackError: +# stack level too deep +# # ./lib/report_formatter/chart_common.rb:555:in `build_performance_chart' +# # ./lib/report_formatter/chart_common.rb:57:in `call' + +describe ReportFormatter::C3Formatter do + include Spec::Support::ReportHelper + + before do + allow(Charting).to receive(:backend).and_return(:c3) + allow(Charting).to receive(:format).and_return(:c3) + end + context '#build_performance_chart_area' do + it "builds a daily chart with all nils" do + report = MiqReport.new( + :db => "VimPerformanceDaily", + :cols => cols = %w(timestamp cpu_usagemhz_rate_average min_cpu_usagemhz_rate_average max_cpu_usagemhz_rate_average trend_max_cpu_usagemhz_rate_average resource.cpu_usagemhz_rate_average_high_over_time_period resource.cpu_usagemhz_rate_average_low_over_time_period), + :include => { + "resource" => { + "columns" => %w(cpu_usagemhz_rate_average_high_over_time_period cpu_usagemhz_rate_average_low_over_time_period derived_memory_used_high_over_time_period derived_memory_used_low_over_time_period), + } + }, + :col_order => cols, + :headers => ["Date/Time", "Avg Used", "Max Available", "Max Reserved", "Trend Max Used", "foo", "bar"], + :order => "ascending", + :sortby => "timestamp", + :group => "n", + :graph => { + :type => "Line", + :columns => %w(cpu_usagemhz_rate_average min_cpu_usagemhz_rate_average max_cpu_usagemhz_rate_average trend_max_cpu_usagemhz_rate_average resource.cpu_usagemhz_rate_average_high_over_time_period resource.cpu_usagemhz_rate_average_low_over_time_period), + :legends => nil, + :max_col_size => nil + }, + :dims => nil, + :col_formats => nil, + :col_options => nil, + :rpt_options => nil, + ) + + report.table = Ruport::Data::Table.new( + :column_names => %w(timestamp cpu_usagemhz_rate_average min_cpu_usagemhz_rate_average max_cpu_usagemhz_rate_average trend_max_cpu_usagemhz_rate_average), + :data => [["Sun, 20 Mar 2016 00:00:00 UTC +00:00", 0.0, nil, nil, 0]] + ) + + expect_any_instance_of(described_class).to receive(:build_performance_chart_area).once.and_call_original + render_report(report) { |e| e.options.graph_options[:chart_type] = :performance } + expect(report.chart[:data]).to be + end + end +end diff --git a/spec/lib/manageiq/report_formater/text_formatter_spec.rb b/spec/lib/manageiq/report_formater/text_formatter_spec.rb new file mode 100644 index 00000000000..33f27c04921 --- /dev/null +++ b/spec/lib/manageiq/report_formater/text_formatter_spec.rb @@ -0,0 +1,78 @@ +describe ReportFormatter::ReportText do + include Spec::Support::ReportHelper + + before do + allow(Charting).to receive(:backend).and_return(:text) + allow(Charting).to receive(:format).and_return(:text) + end + + it "expands report width for really long filter condition" do + report = null_data_chart_with_complex_condition + Timecop.freeze do + result = render_report(report) { |r| r.options.ignore_table_width = true } + + expected = <<~TABLE + +--------------------------------------------------------------------+ + | Name | Hardware CPU Speed | Hardware Number of CPUs | Hardware RAM | + +--------------------------------------------------------------------+ + | Чук | | 4 | 6 GB | + | Гек | | | 1 GB | + +--------------------------------------------------------------------+ + +--------------------------------------------------------------------+ + |Report based filter fields: | + |( Performance - VM : Activity Sample - Timestamp (Day/Time) IS "Last Hour" AND Performance - VM : CPU - Usage Rate for Collected Intervals (%) > 0 AND Performance - VM.VM and Instance : Type INCLUDES "Amazon" )| + +--------------------------------------------------------------------+ + | #{described_class.format_timezone(Time.zone.now)} | + +--------------------------------------------------------------------+ + TABLE + result.lines.each_with_index do |line, index| + expect(line.strip).to eq(expected.lines[index].strip) + end + end + end + + it "renders basic text report" do + report = null_data_chart + Timecop.freeze do + result = render_report(report) + expected = <<~TABLE + +--------------------------------------------------------------------+ + | Name | Hardware CPU Speed | Hardware Number of CPUs | Hardware RAM | + +--------------------------------------------------------------------+ + | Чук | | 4 | 6 GB | + | Гек | | | 1 GB | + +--------------------------------------------------------------------+ + +--------------------------------------------------------------------+ + | #{described_class.format_timezone(Time.zone.now)} | + +--------------------------------------------------------------------+ + TABLE + result.lines.each_with_index do |line, index| + expect(line.strip).to eq(expected.lines[index].strip) + end + end + end + + it "renders report with basic filter condition" do + report = null_data_chart_with_basic_condition + Timecop.freeze do + result = render_report(report) + expected = <<~TABLE + +--------------------------------------------------------------------+ + | Name | Hardware CPU Speed | Hardware Number of CPUs | Hardware RAM | + +--------------------------------------------------------------------+ + | Чук | | 4 | 6 GB | + | Гек | | | 1 GB | + +--------------------------------------------------------------------+ + +--------------------------------------------------------------------+ + |Report based filter fields: | + | Name INCLUDES "Amazon" | + +--------------------------------------------------------------------+ + | #{described_class.format_timezone(Time.zone.now)} | + +--------------------------------------------------------------------+ + TABLE + result.lines.each_with_index do |line, index| + expect(line.strip).to eq(expected.lines[index].strip) + end + end + end +end diff --git a/spec/lib/manageiq/report_formater/timeline_message_spec.rb b/spec/lib/manageiq/report_formater/timeline_message_spec.rb new file mode 100644 index 00000000000..c84460caad1 --- /dev/null +++ b/spec/lib/manageiq/report_formater/timeline_message_spec.rb @@ -0,0 +1,17 @@ +describe ReportFormatter::TimelineMessage do + context "#message_html" do + context "for unknown column names" do + subject { described_class.new({'column' => @value}, nil, {:time_zone => 'Mexico City'}, nil).message_html('column') } + + it 'returns a string for text columns' do + @value = '123""45/\\' + expect(subject).to eq '123""45/\\' + end + + it 'returns formatted time for time columns' do + @value = Time.new(2002, 10, 31, 2, 2, 2, '+02:00') + expect(subject).to eq '2002-10-30 18:02:02 CST' + end + end + end +end diff --git a/spec/lib/manageiq/report_formater/timeline_spec.rb b/spec/lib/manageiq/report_formater/timeline_spec.rb new file mode 100644 index 00000000000..b1ac3f2cea7 --- /dev/null +++ b/spec/lib/manageiq/report_formater/timeline_spec.rb @@ -0,0 +1,285 @@ +describe ReportFormatter::TimelineMessage do + describe '#message_html on container event' do + row = {} + let(:ems) { FactoryBot.create(:ems_redhat, :id => 42) } + let(:event) do + FactoryBot.create(:ems_event, + :event_type => 'CONTAINER_CREATED', + :ems_id => 6, + :container_group_name => 'hawkular-cassandra-1-wb1z6', + :container_namespace => 'openshift-infra', + :container_name => 'hawkular-cassandra-1', + :ext_management_system => ems) + end + + flags = {:ems_cloud => false, + :ems_container => true, + :time_zone => nil} + tests = {'event_type' => 'test timeline', + 'ext_management_system.name' => 'test timeline', + 'container_node_name' => ''} + + tests.each do |column, href| + it "Evaluate column #{column} content" do + row[column] = 'test timeline' + val = ReportFormatter::TimelineMessage.new(row, event, flags, 'EmsEvent').message_html(column) + expect(val).to eq(href) + end + end + end + + describe '#message_html on vm event' do + row = {} + let(:vm) { FactoryBot.create(:vm_redhat, :id => 42) } + let(:event) do + FactoryBot.create(:ems_event, + :event_type => 'VM_CREATED', + :vm_or_template => vm) + end + + flags = {:ems_cloud => false, + :ems_container => false, + :time_zone => nil} + tests = {'event_type' => 'test timeline', + 'ext_management_system.name' => '', + 'src_vm_name' => 'test timeline'} + + tests.each do |column, href| + it "Evaluate column #{column} content" do + row[column] = 'test timeline' + val = ReportFormatter::TimelineMessage.new(row, event, flags, 'EmsEvent').message_html(column) + expect(val).to eq(href) + end + end + end + + describe '#message_html on policy event' do + row = {} + let(:vm) { FactoryBot.create(:vm_redhat, :id => 42, :name => 'Test VM') } + let(:event) do + FactoryBot.create(:policy_event, + :event_type => 'vm_poweroff', + :target_id => 42, + :target_name => vm.name, + :target_class => 'VmOrTemplate') + end + + tests = {'event_type' => 'vm_poweroff', + 'target_name' => 'Test VM
VM or Template: Test VM
Assigned Profiles: '} + + context 'policy profile assigned' do + let(:event_content) { FactoryBot.create(:policy_event_content, :resource => policy_set) } + let(:policy_set) { FactoryBot.create(:miq_policy_set) } + + before { event.contents << event_content } + + subject { ReportFormatter::TimelineMessage.new({'event_type' => 'vm_poweroff'}, event, {}, 'PolicyEvent').message_html('target_name') } + + it 'generates a link to the affected policy profile' do + is_expected.to include("?profile=#{policy_set.id}") + end + end + + tests.each do |column, href| + it "Evaluate column #{column} content" do + row[column] = 'vm_poweroff' + val = ReportFormatter::TimelineMessage.new(row, event, {}, 'PolicyEvent').message_html(column) + expect(val).to eq(href) + end + end + end + + describe '#events count for different categories' do + let(:ems) { FactoryBot.create(:ems_redhat, :name => 'foobar') } + + def stub_ems_event(event_type) + EventStream.new(:event_type => event_type, :ems_id => ems.id) + end + + before do + @report = FactoryBot.create(:miq_report, + :db => "EventStream", + :col_order => %w(id name event_type timestamp), + :headers => %w(id name event_type timestamp), + :timeline => {:field => "EmsEvent-timestamp", :position => "Last"}) + @report.rpt_options = {:categories => {:power => {:display_name => "Power Activity", + :include_set => %w(VmPoweredOffEvent VmPoweredOnEvent), + :regexes => []}, + :snapshot => {:display_name => "Snapshot Activity", + :include_set => %w(AlarmCreatedEvent AlarmRemovedEvent), + :regexes => []}}} + + data = [] + 30.times do + data.push(Ruport::Data::Record.new("id" => stub_ems_event("VmPoweredOffEvent").id, + "name" => "Baz", + "event_type" => "VmPoweredOffEvent", + "timestamp" => Time.zone.now)) + end + + 15.times do + data.push(Ruport::Data::Record.new("id" => stub_ems_event("AlarmCreatedEvent").id, + "name" => "Baz", + "event_type" => "AlarmCreatedEvent", + "timestamp" => Time.zone.now)) + end + + @report.table = Ruport::Data::Table.new( + :column_names => %w(id name event_type timestamp), + :data => data + ) + end + + it 'shows correct count of timeline events based on categories' do + formatter = ReportFormatter::ReportTimeline.new + formatter.options.mri = @report + events = formatter.build_document_body + expect(JSON.parse(events)[0]["data"][0].length).to eq(30) + expect(JSON.parse(events)[1]["data"][0].length).to eq(15) + end + + it 'shows correct count of timeline events together for report object with no categories' do + @report.rpt_options = {} + formatter = ReportFormatter::ReportTimeline.new + formatter.options.mri = @report + events = formatter.build_document_body + expect(JSON.parse(events)[0]["data"][0].length).to eq(45) + end + + it 'shows correct count of timeline events for timeline based report when rpt_options is nil' do + @report.rpt_options = nil + formatter = ReportFormatter::ReportTimeline.new + formatter.options.mri = @report + events = formatter.build_document_body + expect(JSON.parse(events)[0]["data"][0].length).to eq(45) + end + end + + describe '#events count for regex categories' do + let(:ems) { FactoryBot.create(:ems_redhat) } + + def stub_ems_event(event_type) + EventStream.new(:event_type => event_type, :ems_id => ems.id) + end + + before do + @report = FactoryBot.create( + :miq_report, + :db => "EventStream", + :col_order => %w(id name event_type timestamp), + :headers => %w(id name event_type timestamp), + :timeline => {:field => "EmsEvent-timestamp", :position => "Last"} + ) + @report.rpt_options = { + :categories => { + :power => { + :display_name => "Power Activity", + :include_set => [], + :regexes => [/Event$/] + }, + :snapshot => { + :display_name => "Snapshot Activity", + :include_set => %w(AlarmCreatedEvent AlarmRemovedEvent), + :regexes => [] + } + } + } + + data = [] + (1..5).each do |n| + event_type = "VmPower#{n}Event" + data.push( + Ruport::Data::Record.new( + "id" => stub_ems_event(event_type).id, + "name" => "Baz", + "event_type" => event_type, + "timestamp" => Time.zone.now + ) + ) + end + + 7.times do + data.push( + Ruport::Data::Record.new( + "id" => stub_ems_event("AlarmRemovedEvent").id, + "name" => "Baz", + "event_type" => "AlarmRemovedEvent", + "timestamp" => Time.zone.now + ) + ) + end + + @report.table = Ruport::Data::Table.new( + :column_names => %w(id name event_type timestamp), + :data => data + ) + end + + it 'shows correct count of timeline events based on categories' do + formatter = ReportFormatter::ReportTimeline.new + formatter.options.mri = @report + events = formatter.build_document_body + expect(JSON.parse(events)[0]["data"][0].length).to eq(5) + expect(JSON.parse(events)[1]["data"][0].length).to eq(7) + end + + it 'shows correct count of timeline events together for report object with no categories' do + @report.rpt_options = {} + formatter = ReportFormatter::ReportTimeline.new + formatter.options.mri = @report + events = formatter.build_document_body + expect(JSON.parse(events)[0]["data"][0].length).to eq(12) + end + + it 'shows correct count of timeline events for timeline based report when rpt_options is nil' do + @report.rpt_options = nil + formatter = ReportFormatter::ReportTimeline.new + formatter.options.mri = @report + events = formatter.build_document_body + expect(JSON.parse(events)[0]["data"][0].length).to eq(12) + end + end +end + +describe '#set data for headers that exist in col headers' do + let(:ems) { FactoryBot.create(:ems_amazon) } + + def stub_ems_event(event_type) + EventStream.create!(:event_type => event_type, :ems_id => ems.id) + end + + before do + @report = FactoryBot.create(:miq_report, + :db => "EventStream", + :col_order => %w(id name event_type timestamp vm_location), + :headers => %w(id name event_type timestamp vm_location), + :timeline => {:field => "EmsEvent-timestamp", :position => "Last"}) + @report.rpt_options = {:categories => {:power => {:display_name => "Power Activity", + :include_set => %w(VmPoweredOffEvent VmPoweredOnEvent), + :regexes => []}, + :snapshot => {:display_name => "Snapshot Activity", + :include_set => %w(AlarmCreatedEvent AlarmRemovedEvent), + :regexes => []}}} + + data = [Ruport::Data::Record.new("id" => stub_ems_event("VmPoweredOffEvent").id, + "name" => "Baz", + "event_type" => "VmPoweredOffEvent", + "vm_location" => "foo", + "timestamp" => Time.zone.now)] + + @report.table = Ruport::Data::Table.new( + :column_names => %w(id name event_type timestamp vm_location), + :data => data + ) + end + + it 'shows headers only if they exist in report col headers' do + @report.rpt_options = nil + formatter = ReportFormatter::ReportTimeline.new + formatter.options.mri = @report + events = formatter.build_document_body + json = JSON.parse(events)[0]["data"][0][0]["event"] + expect(json["vm_location"]["text"]).to eq("Source Instance Location") + expect(json["vm_location"]["value"]).to eq("foo") + end +end diff --git a/spec/support/report_helper.rb b/spec/support/report_helper.rb new file mode 100644 index 00000000000..2a563ddb2fc --- /dev/null +++ b/spec/support/report_helper.rb @@ -0,0 +1,324 @@ +module Spec + module Support + module ReportHelper + def render_report(report) + ReportFormatter::ReportRenderer.render(Charting.format) do |e| + e.options.mri = report + e.options.show_title = true + e.options.graph_options = MiqReport.graph_options + e.options.theme = 'miq' + yield e if block_given? + end + end + + def numeric_charts_2d_from_summaries(other) + report = MiqReport.new( + :db => "Vm", + :sortby => ["ext_management_system.name"], + :order => "Descending", + :cols => %w(name num_cpu), + :include => {"ext_management_system" => {"columns" => ["name"]}}, + :col_order => %w(name num_cpu ext_management_system.name), + :headers => ["Name", "Number of CPUs", "Cloud/Infrastructure Provider Name"], + :dims => 1, + :group => "y", + :rpt_options => {:summary => {:hide_detail_rows => false}}, + :col_options => {"num_cpu" => {:grouping => %i(avg max min total)}, + "name" => {:break_label => "Cloud/Infrastructure Provider : Name: "}}, + :graph => {:type => "Column", :mode => "values", :column => "Vm-num_cpu:total", :count => 2, :other => other}, + :extras => {}, + ) + + report.table = Ruport::Data::Table.new( + :column_names => %w(name num_cpu ext_management_system.name id), + :data => [ + ["bar", 1, system_name_1 = 'blah', 352], + ["foo", 3, system_name_1, 353], + ["pat", 1, system_name_2 = 'bleh', 354], + ["mat", 1, system_name_2, 355], + ["cuk", 1, system_name_3 = 'blrg', 356], + ["gek", 2, system_name_3, 357], + ["tik", 1, system_name_4 = 'blud', 358], + ["tak", 1, system_name_4, 359], + ], + ) + report + end + + def numeric_chart_simple + report = MiqReport.new( + :db => "Host", + :cols => %w(name ram_size), + :col_order => %w(name ram_size), + :headers => ["Name", "RAM Size (MB)"], + :order => "Ascending", + :sortby => %w(name), + :group => nil, + :graph => {:type => "Bar", :mode => "values", :column => "Host-ram_size", :count => 10, :other => false}, + :dims => 1, + :col_options => {}, + :extras => {}, + ) + + report.table = Ruport::Data::Table.new( + :column_names => %w(name ram_size id), + :data => [ + ['jenda', 512, 1], + ['ladas', 1024, 2], + ['joker', 2024, 3], + ] + ) + report + end + + def null_data_chart_with_basic_condition + exp = YAML.load('--- !ruby/object:MiqExpression + exp: + INCLUDES: + field: Name + value: Amazon + ') + null_data_chart.tap { |r| r.update(:conditions => exp) } + end + + def null_data_chart_with_complex_condition + exp = YAML.load('--- !ruby/object:MiqExpression + exp: + and: + - IS: + field: VmPerformance-timestamp + value: Last Hour + - ">": + value: "0" + field: VmPerformance-cpu_usage_rate_average + - INCLUDES: + field: VmPerformance.vm-type + value: Amazon + ') + null_data_chart.tap { |r| r.update(:conditions => exp) } + end + + def numeric_chart_simple_with_long_strings + report = MiqReport.new( + :db => "Host", + :cols => %w(name ram_size), + :col_order => %w(name ram_size), + :headers => [long_header, "RAM Size (MB)"], + :order => "Ascending", + :sortby => %w(name), + :group => nil, + :graph => {:type => "Bar", :mode => "values", :column => "Host-ram_size", :count => 10, :other => false}, + :dims => 1, + :col_options => {}, + :extras => {}, + ) + + report.table = Ruport::Data::Table.new( + :column_names => %w(name ram_size id), + :data => [ + [long_category, 512, 1], + ['ladas', 1024, 2], + ['joker', 2024, 3], + ] + ) + report + end + + def null_data_chart + report = MiqReport.new( + :db => "Vm", + :cols => %w(name), + :include => {"hardware" => {"columns" => %w(cpu_speed cpu_sockets memory_mb)}}, + :col_order => %w(name hardware.cpu_speed hardware.cpu_sockets hardware.memory_mb), + :headers => ["Name", "Hardware CPU Speed", "Hardware Number of CPUs", "Hardware RAM"], + :order => "Ascending", + :sortby => %w(name), + :graph => {:type => "Bar", :mode => "values", :column => "Vm.hardware-cpu_sockets", :count => 10, :other => true}, + :dims => 1, + :col_options => {}, + :rpt_options => {}, + :extras => {}, + ) + + report.table = Ruport::Data::Table.new( + :column_names => %w(name hardware.cpu_speed hardware.cpu_sockets hardware.memory_mb id), + :data => [ + ["Чук", nil, 4, 6_144, 42], + ["Гек", nil, nil, 1_024, 49], + ], + ) + report + end + + def numeric_chart_simple2(other) + report = MiqReport.new( + :db => "Host", + :sortby => %w(name), + :order => "Descending", + :cols => %w(name v_total_vms), + :include => {}, + :col_order => %w(name v_total_vms), + :headers => ["Name", "Total VMs"], + :dims => 1, + :group => nil, + :rpt_options => {}, + :col_options => {}, + :graph => {:type => "Bar", :mode => "values", :column => "Host-v_total_vms", :count => 4, :other => other}, + :extras => {}, + ) + + report.table = Ruport::Data::Table.new( + :column_names => %w(name v_total_vms id), + :data => [ + ["bar", 15, 1352], + ["foo", 14, 1353], + ["foo", 3, 1354], + ["foo", 2, 1355], + ["foo", 1, 1356], + ["foo", 0, 1357], + ], + ) + report + end + + def numeric_chart_3d(other) + report = MiqReport.new( + :db => "Vm", + :cols => %w(os_image_name mem_cpu), + :include => {"ext_management_system" => {"columns" => ["name"]}}, + :col_order => %w(ext_management_system.name os_image_name mem_cpu), + :headers => ["Cloud/Infrastructure Provider Name", "OS Name", "Memory"], + :order => "Ascending", + :sortby => %w(ext_management_system.name os_image_name), + :group => "y", + :graph => {:type => "StackedBar", :mode => "values", :column => "Vm-mem_cpu:total", :count => 2, :other => other}, + :dims => 2, + :col_options => {"name" => {:break_label => "Cloud/Infrastructure Provider : Name: "}, "mem_cpu" => {:grouping => [:total]}}, + :rpt_options => {:summary => {:hide_detail_rows => false}}, + :extras => {}, + ) + + report.table = Ruport::Data::Table.new( + :column_names => %w(os_image_name mem_cpu ext_management_system.name id), + :data => [ + ["linux_centos", 6_144, "MTC-RHEVM-3.0", 67], + ["linux_centos", 512, "MTC-RHEVM-3.0", 167], + ["windows", 1_024, "MTC-RHEVM-3.0", 68], + ["linux_centos", 4_096, "openstack", 70], + ["windows", 2_048, "openstack", 69], + ["windows", 1_024, "openstack", 71], + ["linux_centos", 1_024, "ec2", 72], + ["", 0, "", 79], + ], + ) + report + end + + def chart_with_namespace_prefix + report = MiqReport.new( + :db => "ManageIQ::Providers::InfraManager::Vm", + :cols => %w(os_image_name cpu_total_cores num_cpu), + :include => {"host" => {"columns" => %w(name)}}, + :col_order => %w(os_image_name host.name cpu_total_cores num_cpu), + :headers => ["OS Name", "Host / Node Name", "Number of CPU Cores", "Number of CPUs"], + :order => "Ascending", + :sortby => %w(host.name os_image_name), + :group => "y", + :graph => {:type => "Bar", :mode => "values", :column => "ManageIQ::Providers::InfraManager::Vm-num_cpu:total", :count => 10, :other => true}, + :dims => 2, + :col_options => {"name" => {:break_label => "Host / Node : Name: "}, "num_cpu" => {:grouping => [:total]}}, + :rpt_options => {:summary => {:hide_detail_rows => false}}, + :extras => {} + ) + + report.table = Ruport::Data::Table.new( + :column_names => %w(os_image_name cpu_total_cores num_cpu host.name id), + :data => [ + ["linux_centos", 8, 2, "MTC-RHEVM-3.0", 67], + ] + ) + report + end + + def cu_chart_without_grouping + report = MiqReport.new( + :db => "VimPerformanceDaily", + :cols => %w(timestamp cpu_usagemhz_rate_average max_derived_cpu_available), + :include => {"resource" => {"columns" => %w(cpu_usagemhz_rate_average_high_over_time_period cpu_usagemhz_rate_average_low_over_time_period)}}, + :col_order => %w(timestamp cpu_usagemhz_rate_average max_derived_cpu_available), + :headers => ["Date/Time", "Avg Used", "Max Available"], + :order => "Ascending", + :sortby => %w(timestamp), + :group => "n", + :graph => {:type => "Line", :columns => %w(cpu_usagemhz_rate_average max_derived_cpu_available)}, + :extras => {:trend => {"trend_max_cpu_usagemhz_rate_average|max_derived_cpu_available"=>"Trending Down"}} + ) + + report.table = Ruport::Data::Table.new( + :column_names => %w(timestamp cpu_usagemhz_rate_average max_derived_cpu_available), + :data => [ + [Time.zone.local(2017, 8, 19, 0, 0, 0), 19_986.0, 41_584.0], + [Time.zone.local(2017, 8, 20, 0, 0, 0), 205_632.0, 41_584.0] + ] + ) + report + end + + def cu_chart_with_grouping + report = MiqReport.new( + :db => "VimPerformanceDaily", + :cols => %w(timestamp cpu_usagemhz_rate_average__none_ max_derived_cpu_available_xa), + :include => {"resource" => {"columns" => %w(cpu_usagemhz_rate_average_high_over_time_period cpu_usagemhz_rate_average_low_over_time_period)}}, + :col_order => %w(timestamp cpu_usagemhz_rate_average__none_ max_derived_cpu_available_xa), + :headers => ["Date/Time", "Avg Used", "Max Available"], + :order => "Ascending", + :sortby => %w(timestamp), + :group => "n", + :graph => {:type => "Line", :columns => %w(cpu_usagemhz_rate_average__none_ max_derived_cpu_available_xa)}, + :extras => {:trend => {"trend_max_cpu_usagemhz_rate_average|max_derived_cpu_available"=>"Trending Down"}}, + :performance => {:group_by_category=>"environment"} + ) + + report.table = Ruport::Data::Table.new( + :column_names => %w(timestamp cpu_usagemhz_rate_average__none_ max_derived_cpu_available_xa), + :data => [ + [Time.zone.local(2017, 8, 19, 0, 0, 0), 19_986.0, 41_584.0], + [Time.zone.local(2017, 8, 20, 0, 0, 0), 205_632.0, 41_584.0] + ] + ) + report + end + + def cu_chart_with_no_data + report = MiqReport.new( + :db => "VimPerformanceDaily", + :cols => %w(timestamp cpu_usagemhz_rate_average max_derived_cpu_available), + :include => {"resource" => {"columns" => %w(cpu_usagemhz_rate_average_high_over_time_period cpu_usagemhz_rate_average_low_over_time_period)}}, + :col_order => %w(timestamp cpu_usagemhz_rate_average max_derived_cpu_available), + :headers => ["Date/Time", "Avg Used", "Max Available"], + :order => "Ascending", + :sortby => %w(timestamp), + :group => "n", + :graph => {:type => "Line", :columns => %w(cpu_usagemhz_rate_average max_derived_cpu_available)}, + :extras => {:trend => {"trend_max_cpu_usagemhz_rate_average|max_derived_cpu_available"=>"Trending Down"}} + ) + + report.table = Ruport::Data::Table.new( + :column_names => %w(timestamp cpu_usagemhz_rate_average max_derived_cpu_available), + :data => [] + ) + report + end + + def long_category + 'Daenerys Targaryen, the First of Her Name, Queen of Meereen, Queen of the Andals and the Rhoynar and the First Men,\ + Lord of the Seven Kingdoms, Protector of the Realm, Khaleesi of the Great Grass Sea, called Daenerys Stormborn, the Unburnt,\ + Mother of Dragons.' + end + + def long_header + "Here is header loooooong as hell" + end + end + end +end From 75692ceed66f71672959d0f3a8a6799cda791c67 Mon Sep 17 00:00:00 2001 From: Joe Rafaniello Date: Tue, 25 Feb 2020 14:12:03 -0500 Subject: [PATCH 2/4] Move spec locations based on namespacing Related to: https://github.com/ManageIQ/manageiq/issues/19674 (missing ReportFormatter for backend workers) https://github.com/ManageIQ/manageiq/issues/19863 (move code from UI-classic that can be called from either the frontend or backend) --- .../c3_formatter_spec.rb => reporting/formatter/c3_spec.rb} | 0 .../{report_formater => reporting/formatter}/chart_common_spec.rb | 0 .../text_formatter_spec.rb => reporting/formatter/text_spec.rb} | 0 .../formatter}/timeline_message_spec.rb | 0 .../{report_formater => reporting/formatter}/timeline_spec.rb | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename spec/lib/manageiq/{report_formater/c3_formatter_spec.rb => reporting/formatter/c3_spec.rb} (100%) rename spec/lib/manageiq/{report_formater => reporting/formatter}/chart_common_spec.rb (100%) rename spec/lib/manageiq/{report_formater/text_formatter_spec.rb => reporting/formatter/text_spec.rb} (100%) rename spec/lib/manageiq/{report_formater => reporting/formatter}/timeline_message_spec.rb (100%) rename spec/lib/manageiq/{report_formater => reporting/formatter}/timeline_spec.rb (100%) diff --git a/spec/lib/manageiq/report_formater/c3_formatter_spec.rb b/spec/lib/manageiq/reporting/formatter/c3_spec.rb similarity index 100% rename from spec/lib/manageiq/report_formater/c3_formatter_spec.rb rename to spec/lib/manageiq/reporting/formatter/c3_spec.rb diff --git a/spec/lib/manageiq/report_formater/chart_common_spec.rb b/spec/lib/manageiq/reporting/formatter/chart_common_spec.rb similarity index 100% rename from spec/lib/manageiq/report_formater/chart_common_spec.rb rename to spec/lib/manageiq/reporting/formatter/chart_common_spec.rb diff --git a/spec/lib/manageiq/report_formater/text_formatter_spec.rb b/spec/lib/manageiq/reporting/formatter/text_spec.rb similarity index 100% rename from spec/lib/manageiq/report_formater/text_formatter_spec.rb rename to spec/lib/manageiq/reporting/formatter/text_spec.rb diff --git a/spec/lib/manageiq/report_formater/timeline_message_spec.rb b/spec/lib/manageiq/reporting/formatter/timeline_message_spec.rb similarity index 100% rename from spec/lib/manageiq/report_formater/timeline_message_spec.rb rename to spec/lib/manageiq/reporting/formatter/timeline_message_spec.rb diff --git a/spec/lib/manageiq/report_formater/timeline_spec.rb b/spec/lib/manageiq/reporting/formatter/timeline_spec.rb similarity index 100% rename from spec/lib/manageiq/report_formater/timeline_spec.rb rename to spec/lib/manageiq/reporting/formatter/timeline_spec.rb From c59ae9e6d871163bbb07296ce6bc4f3934e1aa85 Mon Sep 17 00:00:00 2001 From: Joe Rafaniello Date: Tue, 25 Feb 2020 14:55:51 -0500 Subject: [PATCH 3/4] Namespace all the things. From #19863 Related to: https://github.com/ManageIQ/manageiq/issues/19674 (missing ReportFormatter for backend workers) https://github.com/ManageIQ/manageiq/issues/19863 (move code from UI-classic that can be called from either the frontend or backend) --- app/models/miq_report/formatters/graph.rb | 2 +- app/models/miq_widget/chart_content.rb | 2 +- lib/charting.rb | 6 + lib/manageiq/reporting/charting.rb | 60 +- .../reporting/charting/c3_charting.rb | 160 ++-- lib/manageiq/reporting/formatter.rb | 44 +- lib/manageiq/reporting/formatter/c3.rb | 328 +++---- lib/manageiq/reporting/formatter/c3_helper.rb | 32 + lib/manageiq/reporting/formatter/c3_series.rb | 42 +- .../reporting/formatter/chart_common.rb | 798 +++++++++--------- lib/manageiq/reporting/formatter/converter.rb | 32 +- lib/manageiq/reporting/formatter/html.rb | 110 +-- .../reporting/formatter/report_renderer.rb | 14 +- lib/manageiq/reporting/formatter/text.rb | 440 +++++----- lib/manageiq/reporting/formatter/timeline.rb | 330 ++++---- .../reporting/formatter/timeline_message.rb | 252 +++--- lib/report_formatter.rb | 6 + .../manageiq/reporting/formatter/c3_spec.rb | 6 +- .../reporting/formatter/chart_common_spec.rb | 6 +- .../manageiq/reporting/formatter/text_spec.rb | 6 +- .../formatter/timeline_message_spec.rb | 2 +- .../reporting/formatter/timeline_spec.rb | 24 +- spec/models/miq_report/charting_spec.rb | 4 +- spec/models/miq_report_result_spec.rb | 2 +- spec/models/miq_widget/chart_content_spec.rb | 2 +- spec/support/report_helper.rb | 2 +- 26 files changed, 1414 insertions(+), 1298 deletions(-) create mode 100644 lib/charting.rb create mode 100644 lib/manageiq/reporting/formatter/c3_helper.rb create mode 100644 lib/report_formatter.rb diff --git a/app/models/miq_report/formatters/graph.rb b/app/models/miq_report/formatters/graph.rb index a10a28e76d8..7e698003448 100644 --- a/app/models/miq_report/formatters/graph.rb +++ b/app/models/miq_report/formatters/graph.rb @@ -10,7 +10,7 @@ def graph_options(options = nil) end def to_chart(theme = nil, show_title = false, graph_options = nil) - ReportFormatter::ReportRenderer.render(Charting.format) do |e| + ManageIQ::Reporting::Formatter::ReportRenderer.render(ManageIQ::Reporting::Charting.format) do |e| e.options.mri = self e.options.show_title = show_title e.options.graph_options = graph_options unless graph_options.nil? diff --git a/app/models/miq_widget/chart_content.rb b/app/models/miq_widget/chart_content.rb index 437cb117603..cd79b0a32a6 100644 --- a/app/models/miq_widget/chart_content.rb +++ b/app/models/miq_widget/chart_content.rb @@ -6,6 +6,6 @@ def generate(user_or_group) theme ||= "MIQ" report.to_chart(theme, false, MiqReport.graph_options) - Charting.serialized(report.chart) + ManageIQ::Reporting::Charting.serialized(report.chart) end end diff --git a/lib/charting.rb b/lib/charting.rb new file mode 100644 index 00000000000..64204cd5d79 --- /dev/null +++ b/lib/charting.rb @@ -0,0 +1,6 @@ +# If code uses the old constant name: +# * Rails will autoload it and start here. +# * We assign the old toplevel constant to the new constant. +# * We can't include rails deprecate_constant globally, so we use ruby's. +Charting = ManageIQ::Reporting::Charting +Object.deprecate_constant :Charting diff --git a/lib/manageiq/reporting/charting.rb b/lib/manageiq/reporting/charting.rb index f00ab747f7a..96d24c2fe93 100644 --- a/lib/manageiq/reporting/charting.rb +++ b/lib/manageiq/reporting/charting.rb @@ -1,34 +1,38 @@ -class Charting - class << self - extend Forwardable - delegate [ - :backend, # charting backend name; FIXME: remove this method - :render_format, - :format, # format for Ruport renderer - :load_helpers, - :data_ok?, - :sample_chart, - :chart_names_for_select, - :chart_themes_for_select, - :serialized, - :deserialized, - :js_load_statement # javascript statement to reload charts - ] => :instance - end +module ManageIQ + module Reporting + class Charting + class << self + extend Forwardable + delegate [ + :backend, # charting backend name; FIXME: remove this method + :render_format, + :format, # format for Ruport renderer + :load_helpers, + :data_ok?, + :sample_chart, + :chart_names_for_select, + :chart_themes_for_select, + :serialized, + :deserialized, + :js_load_statement # javascript statement to reload charts + ] => :instance + end - # discovery - # - # - def self.instance - @instance ||= new - end + # discovery + # + # + def self.instance + @instance ||= new + end - def self.new - self == Charting ? detect_available_plugin.new : super - end + def self.new + self == ManageIQ::Reporting::Charting ? detect_available_plugin.new : super + end - def self.detect_available_plugin - subclasses.select(&:available?).max_by(&:priority) + def self.detect_available_plugin + subclasses.select(&:available?).max_by(&:priority) + end + end end end diff --git a/lib/manageiq/reporting/charting/c3_charting.rb b/lib/manageiq/reporting/charting/c3_charting.rb index cb790cf759c..ff8324b94a0 100644 --- a/lib/manageiq/reporting/charting/c3_charting.rb +++ b/lib/manageiq/reporting/charting/c3_charting.rb @@ -1,92 +1,96 @@ -class C3Charting < Charting - # for Charting.detect_available_plugin - def self.available? - true - end +module ManageIQ + module Reporting + class C3Charting < ManageIQ::Reporting::Charting + # for Charting.detect_available_plugin + def self.available? + true + end - # for Charting.detect_available_plugin - def self.priority - 1000 - end + # for Charting.detect_available_plugin + def self.priority + 1000 + end - # backend identifier - def backend - :c3 - end + # backend identifier + def backend + :c3 + end - # format for rails' render - def render_format - :json - end + # format for rails' render + def render_format + :json + end - # formatter for Rupport::Controller#render - see lib/report_formatter/... - def format - :c3 - end + # formatter for Rupport::Controller#render - see lib/report_formatter/... + def format + :c3 + end - # called from each ApplicationController instance - def load_helpers(klass) - klass.instance_eval do - helper C3Helper - end - end + # called from each ApplicationController instance + def load_helpers(klass) + klass.instance_eval do + helper ManageIQ::Reporting::Formatter::C3Helper + end + end - def data_ok?(data) - obj = YAML.load(data) - !!obj && obj.kind_of?(Hash) && !obj[:options] - rescue Psych::SyntaxError, ArgumentError - false - end + def data_ok?(data) + obj = YAML.load(data) + !!obj && obj.kind_of?(Hash) && !obj[:options] + rescue Psych::SyntaxError, ArgumentError + false + end - def sample_chart(_options, _report_theme) - sample = { - :data => { - :axis => {}, - :tooltip => {}, - :columns => [ - ['data1', 30, 200, 100, 400, 150, 250], - ['data2', 50, 20, 10, 40, 15, 25], - ['data3', 10, 25, 10, 250, 10, 30] - ], - }, - :miqChart => _options[:graph_type], - :miq => { :zoomed => false } - } - sample[:data][:groups] = [['data1','data2', 'data3']] if _options[:graph_type].include? 'Stacked' - sample - end + def sample_chart(_options, _report_theme) + sample = { + :data => { + :axis => {}, + :tooltip => {}, + :columns => [ + ['data1', 30, 200, 100, 400, 150, 250], + ['data2', 50, 20, 10, 40, 15, 25], + ['data3', 10, 25, 10, 250, 10, 30] + ], + }, + :miqChart => _options[:graph_type], + :miq => { :zoomed => false } + } + sample[:data][:groups] = [['data1','data2', 'data3']] if _options[:graph_type].include? 'Stacked' + sample + end - def js_load_statement(delayed = false) - delayed ? 'setTimeout(function(){ load_c3_charts(); }, 100);' : 'load_c3_charts();' - end + def js_load_statement(delayed = false) + delayed ? 'setTimeout(function(){ load_c3_charts(); }, 100);' : 'load_c3_charts();' + end - # list of available chart types - in options_for_select format - def chart_names_for_select - CHART_NAMES - end + # list of available chart types - in options_for_select format + def chart_names_for_select + CHART_NAMES + end - # list of themes - in options_for_select format - def chart_themes_for_select - [%w(Default default)] - end + # list of themes - in options_for_select format + def chart_themes_for_select + [%w(Default default)] + end - def serialized(data) - data.try(:to_yaml) - end + def serialized(data) + data.try(:to_yaml) + end - def deserialized(data) - YAML.load(data) - end + def deserialized(data) + YAML.load(data) + end - CHART_NAMES = [ - ["Bars (2D)", "Bar"], - ["Bars, Stacked (2D)", "StackedBar"], - ["Columns (2D)", "Column"], - ["Columns, Stacked (2D)", "StackedColumn"], - ["Donut (2D)", "Donut"], - ["Pie (2D)", "Pie"], - ["Line (2D)", "Line"], - ["Area (2D)", "Area"], - ["Area, Stacked (2D)", "StackedArea"], - ] + CHART_NAMES = [ + ["Bars (2D)", "Bar"], + ["Bars, Stacked (2D)", "StackedBar"], + ["Columns (2D)", "Column"], + ["Columns, Stacked (2D)", "StackedColumn"], + ["Donut (2D)", "Donut"], + ["Pie (2D)", "Pie"], + ["Line (2D)", "Line"], + ["Area (2D)", "Area"], + ["Area, Stacked (2D)", "StackedArea"], + ] + end + end end diff --git a/lib/manageiq/reporting/formatter.rb b/lib/manageiq/reporting/formatter.rb index 9398e329cab..b5a9f9f96e8 100644 --- a/lib/manageiq/reporting/formatter.rb +++ b/lib/manageiq/reporting/formatter.rb @@ -1,15 +1,39 @@ include ActionView::Helpers::NumberHelper -require 'report_formatter/report_renderer' -require 'report_formatter/c3' -require 'report_formatter/converter' -require 'report_formatter/html' -require 'report_formatter/text' -require 'report_formatter/timeline' +require_dependency 'manageiq/reporting/formatter/report_renderer' +require_dependency 'manageiq/reporting/formatter/c3' +require_dependency 'manageiq/reporting/formatter/converter' +require_dependency 'manageiq/reporting/formatter/html' +require_dependency 'manageiq/reporting/formatter/text' +require_dependency 'manageiq/reporting/formatter/timeline' +module ManageIQ + module Reporting + module Formatter + BLANK_VALUE = "Unknown" # Chart constant for nil or blank key values + CRLF = "\r\n" + LEGEND_LENGTH = 11 # Top legend text limit + LABEL_LENGTH = 21 # Chart label text limit + end + end +end + +# Deprecate the constants within ReportFormatter with a helpful replacement. module ReportFormatter - BLANK_VALUE = "Unknown" # Chart constant for nil or blank key values - CRLF = "\r\n" - LEGEND_LENGTH = 11 # Top legend text limit - LABEL_LENGTH = 21 # Chart label text limit + include ActiveSupport::Deprecation::DeprecatedConstantAccessor + deprecate_constant 'BLANK_VALUE', 'ManageIQ::Reporting::Formatter::BLANK_VALUE' + deprecate_constant 'CRLF', 'ManageIQ::Reporting::Formatter::CRLF' + deprecate_constant 'LABEL_LENGTH', 'ManageIQ::Reporting::Formatter::LABEL_LENGTH' + deprecate_constant 'LEGEND_LENGTH', 'ManageIQ::Reporting::Formatter::LEGEND_LENGTH' + + deprecate_constant 'C3Formatter', 'ManageIQ::Reporting::Formatter::C3' + deprecate_constant 'C3Series', 'ManageIQ::Reporting::Formatter::C3Series' + deprecate_constant 'C3Charting', 'ManageIQ::Reporting::Formatter::C3Charting' + deprecate_constant 'ChartCommon', 'ManageIQ::Reporting::Formatter::ChartCommon' + deprecate_constant 'Converter', 'ManageIQ::Reporting::Formatter::Converter' + deprecate_constant 'ReportHTML', 'ManageIQ::Reporting::Formatter::HTML' + deprecate_constant 'ReportRenderer', 'ManageIQ::Reporting::Formatter::ReportRenderer' + deprecate_constant 'ReportText', 'ManageIQ::Reporting::Formatter::Text' + deprecate_constant 'ReportTimeline', 'ManageIQ::Reporting::Formatter::Timeline' + deprecate_constant 'TimelineMessage', 'ManageIQ::Reporting::Formatter::TimelineMessage' end diff --git a/lib/manageiq/reporting/formatter/c3.rb b/lib/manageiq/reporting/formatter/c3.rb index 1530a42b51b..94bfec06684 100644 --- a/lib/manageiq/reporting/formatter/c3.rb +++ b/lib/manageiq/reporting/formatter/c3.rb @@ -1,183 +1,189 @@ -module ReportFormatter - class C3Formatter < Ruport::Formatter - include ActionView::Helpers::UrlHelper - include ChartCommon - include MiqReport::Formatting - renders :c3, :for => ReportRenderer - - # series handling methods - def series_class - C3Series - end - - CONVERT_TYPES = { - "ColumnThreed" => "Column", - "ParallelThreedColumn" => "Column", - "StackedThreedColumn" => "StackedColumn", - "PieThreed" => "Pie", - "AreaThreed" => "Area", - "StackedAreaThreed" => "StackedArea" - } - def add_series(label, data) - @counter ||= 0 - @counter += 1 - series_id = @counter.to_s - limit = pie_type? ? LEGEND_LENGTH : LABEL_LENGTH - - if chart_is_2d? - mri.chart[:data][:columns] << [series_id, *data.map { |a| a[:value] }] - mri.chart[:data][:names][series_id] = slice_legend(_(label), limit) - mri.chart[:miq][:name_table][series_id] = label - else - data.each_with_index do |a, index| - id = index.to_s - mri.chart[:data][:columns].push([id, a[:value]]) - mri.chart[:data][:names][id] = slice_legend(_(a[:tooltip]), limit) - mri.chart[:miq][:name_table][id] = a[:tooltip] +require_dependency 'manageiq/reporting/formatter/c3_series' + +module ManageIQ + module Reporting + module Formatter + class C3 < Ruport::Formatter + include ActionView::Helpers::UrlHelper + include ChartCommon + include MiqReport::Formatting + renders :c3, :for => ReportRenderer + + # series handling methods + def series_class + ManageIQ::Reporting::Formatter::C3Series end - end - - if chart_is_stacked? - mri.chart[:data][:groups][0] << series_id - end - end - def add_axis_category_text(categories) - if chart_is_2d? - category_labels = categories.collect { |c| c.kind_of?(Array) ? c.first : c } - limit = pie_type? ? LEGEND_LENGTH : LABEL_LENGTH - mri.chart[:axis][:x][:categories] = category_labels.collect { |c| slice_legend(c, limit) } - mri.chart[:miq][:category_table] = category_labels - end - end - - # report building methods - def build_document_header - super - type = c3_convert_type(mri.graph[:type].to_s) - mri.chart = { - :miqChart => type, - :data => {:columns => [], :names => {}, :empty => {:label => {:text => _('No data available.')}}}, - :axis => {:x => {:tick => {}}, :y => {:tick => {}, :padding => {:bottom => 0}}}, - :tooltip => {:format => {}}, - :miq => {:name_table => {}, :category_table => {}}, - :legend => {} - } - - if chart_is_2d? - mri.chart[:axis][:x] = { - :categories => [], - :tick => {} + CONVERT_TYPES = { + "ColumnThreed" => "Column", + "ParallelThreedColumn" => "Column", + "StackedThreedColumn" => "StackedColumn", + "PieThreed" => "Pie", + "AreaThreed" => "Area", + "StackedAreaThreed" => "StackedArea" } - end + def add_series(label, data) + @counter ||= 0 + @counter += 1 + series_id = @counter.to_s + limit = pie_type? ? LEGEND_LENGTH : LABEL_LENGTH + + if chart_is_2d? + mri.chart[:data][:columns] << [series_id, *data.map { |a| a[:value] }] + mri.chart[:data][:names][series_id] = slice_legend(_(label), limit) + mri.chart[:miq][:name_table][series_id] = label + else + data.each_with_index do |a, index| + id = index.to_s + mri.chart[:data][:columns].push([id, a[:value]]) + mri.chart[:data][:names][id] = slice_legend(_(a[:tooltip]), limit) + mri.chart[:miq][:name_table][id] = a[:tooltip] + end + end + + if chart_is_stacked? + mri.chart[:data][:groups][0] << series_id + end + end - if chart_is_stacked? - mri.chart[:data][:groups] = [[]] - end + def add_axis_category_text(categories) + if chart_is_2d? + category_labels = categories.collect { |c| c.kind_of?(Array) ? c.first : c } + limit = pie_type? ? LEGEND_LENGTH : LABEL_LENGTH + mri.chart[:axis][:x][:categories] = category_labels.collect { |c| slice_legend(c, limit) } + mri.chart[:miq][:category_table] = category_labels + end + end - # chart is numeric - if mri.graph[:mode] == 'values' - custom_format = Array(mri[:col_formats])[Array(mri[:col_order]).index(raw_column_name)] - format, options = javascript_format(mri.graph[:column].split(/(? type, + :data => {:columns => [], :names => {}, :empty => {:label => {:text => _('No data available.')}}}, + :axis => {:x => {:tick => {}}, :y => {:tick => {}, :padding => {:bottom => 0}}}, + :tooltip => {:format => {}}, + :miq => {:name_table => {}, :category_table => {}}, + :legend => {} + } + + if chart_is_2d? + mri.chart[:axis][:x] = { + :categories => [], + :tick => {} + } + end + + if chart_is_stacked? + mri.chart[:data][:groups] = [[]] + end + + # chart is numeric + if mri.graph[:mode] == 'values' + custom_format = Array(mri[:col_formats])[Array(mri[:col_order]).index(raw_column_name)] + format, options = javascript_format(mri.graph[:column].split(/(? format, :options => options} + mri.chart[:axis][:y] = {:tick => {:format => axis_formatter}} + end + end + + # C&U chart + if graph_options[:chart_type] == :performance + unless mri.graph[:type] == 'Donut' || mri.graph[:type] == 'Pie' + mri.chart[:legend] = {:position => 'bottom'} + end + + return if mri.graph[:columns].blank? + column = grouped_by_tag_category? ? mri.graph[:columns][0].split(/_+/)[0..-2].join('_') : mri.graph[:columns][0] + format, options = javascript_format(column, nil) + return unless format + + axis_formatter = {:function => format, :options => options} + mri.chart[:axis][:y][:tick] = {:format => axis_formatter} + mri.chart[:miq][:format] = axis_formatter + end + end - if format - axis_formatter = {:function => format, :options => options} - mri.chart[:axis][:y] = {:tick => {:format => axis_formatter}} + def c3_convert_type(type) + CONVERT_TYPES[type] || type end - end - # C&U chart - if graph_options[:chart_type] == :performance - unless mri.graph[:type] == 'Donut' || mri.graph[:type] == 'Pie' - mri.chart[:legend] = {:position => 'bottom'} + def chart_is_2d? + ['Bar', 'Column', 'StackedBar', 'StackedColumn', 'Line', 'Area', 'StackedArea'].include?(c3_convert_type(mri.graph[:type])) end - return if mri.graph[:columns].blank? - column = grouped_by_tag_category? ? mri.graph[:columns][0].split(/_+/)[0..-2].join('_') : mri.graph[:columns][0] - format, options = javascript_format(column, nil) - return unless format + def chart_is_stacked? + %w(StackedBar StackedColumn StackedArea).include?(mri.graph[:type]) + end - axis_formatter = {:function => format, :options => options} - mri.chart[:axis][:y][:tick] = {:format => axis_formatter} - mri.chart[:miq][:format] = axis_formatter - end - end + # change structure of chart JSON to performance chart with timeseries data + def build_performance_chart_area(maxcols) + super + change_structure_to_timeseries + end - def c3_convert_type(type) - CONVERT_TYPES[type] || type - end + def no_records_found_chart(*) + mri.chart = { + :axis => {:y => {:show => false}}, + :data => {:columns => [], :empty => {:label => {:text => _('No data available.')}}}, + :miq => {:empty => true}, + } + end - def chart_is_2d? - ['Bar', 'Column', 'StackedBar', 'StackedColumn', 'Line', 'Area', 'StackedArea'].include?(c3_convert_type(mri.graph[:type])) - end + def finalize_document + mri.chart + end - def chart_is_stacked? - %w(StackedBar StackedColumn StackedArea).include?(mri.graph[:type]) - end + private + + # change structure of hash from standard chart to timeseries chart + def change_structure_to_timeseries + # add 'x' as first element and move mri.chart[:axis][:x][:categories] to mri.chart[:data][:columns] as first column + x = mri.chart[:axis][:x][:categories] + x.unshift('x') + mri.chart[:data][:columns].unshift(x) + mri.chart[:data][:x] = 'x' + # set x axis type to timeseries and remove categories + mri.chart[:axis][:x] = {:type => 'timeseries', :tick => {}} + # set flag for performance chart + mri.chart[:miq][:performance_chart] = true + # this conditions are taken from build_performance_chart_area method from chart_commons.rb + if mri.db.include?("Daily") || (mri.where_clause && mri.where_clause.include?("daily")) + # set format for parsing + mri.chart[:data][:xFormat] = '%m/%d' + # set format for labels + mri.chart[:axis][:x][:tick][:format] = '%m/%d' + elsif mri.extras[:realtime] == true + mri.chart[:data][:xFormat] = '%H:%M:%S' + mri.chart[:axis][:x][:tick][:format] = '%H:%M:%S' + else + mri.chart[:data][:xFormat] = '%H:%M' + mri.chart[:axis][:x][:tick][:format] = '%H:%M' + end + end - # change structure of chart JSON to performance chart with timeseries data - def build_performance_chart_area(maxcols) - super - change_structure_to_timeseries - end + def build_reporting_chart(_maxcols) + mri.chart[:miq][:expand_tooltip] = true + super + end - def no_records_found_chart(*) - mri.chart = { - :axis => {:y => {:show => false}}, - :data => {:columns => [], :empty => {:label => {:text => _('No data available.')}}}, - :miq => {:empty => true}, - } - end + def build_reporting_chart_numeric(_maxcols) + mri.chart[:miq][:expand_tooltip] = true + super + end - def finalize_document - mri.chart - end + def build_performance_chart_pie(_maxcols) + mri.chart[:miq][:expand_tooltip] = true + super + end - private - - # change structure of hash from standard chart to timeseries chart - def change_structure_to_timeseries - # add 'x' as first element and move mri.chart[:axis][:x][:categories] to mri.chart[:data][:columns] as first column - x = mri.chart[:axis][:x][:categories] - x.unshift('x') - mri.chart[:data][:columns].unshift(x) - mri.chart[:data][:x] = 'x' - # set x axis type to timeseries and remove categories - mri.chart[:axis][:x] = {:type => 'timeseries', :tick => {}} - # set flag for performance chart - mri.chart[:miq][:performance_chart] = true - # this conditions are taken from build_performance_chart_area method from chart_commons.rb - if mri.db.include?("Daily") || (mri.where_clause && mri.where_clause.include?("daily")) - # set format for parsing - mri.chart[:data][:xFormat] = '%m/%d' - # set format for labels - mri.chart[:axis][:x][:tick][:format] = '%m/%d' - elsif mri.extras[:realtime] == true - mri.chart[:data][:xFormat] = '%H:%M:%S' - mri.chart[:axis][:x][:tick][:format] = '%H:%M:%S' - else - mri.chart[:data][:xFormat] = '%H:%M' - mri.chart[:axis][:x][:tick][:format] = '%H:%M' + def grouped_by_tag_category? + !!(mri.performance && mri.performance.fetch_path(:group_by_category)) + end end end - - def build_reporting_chart(_maxcols) - mri.chart[:miq][:expand_tooltip] = true - super - end - - def build_reporting_chart_numeric(_maxcols) - mri.chart[:miq][:expand_tooltip] = true - super - end - - def build_performance_chart_pie(_maxcols) - mri.chart[:miq][:expand_tooltip] = true - super - end - - def grouped_by_tag_category? - !!(mri.performance && mri.performance.fetch_path(:group_by_category)) - end end end diff --git a/lib/manageiq/reporting/formatter/c3_helper.rb b/lib/manageiq/reporting/formatter/c3_helper.rb new file mode 100644 index 00000000000..d1db93d3856 --- /dev/null +++ b/lib/manageiq/reporting/formatter/c3_helper.rb @@ -0,0 +1,32 @@ +module ManageIQ + module Reporting + module Formatter + module C3Helper + def c3chart_remote(url, opts = {}) + chart_id = opts[:id] || ('chart' + rand(10**8).to_s) + + content_tag(:div, '', :id => chart_id) + + javascript_tag(<<~EOJ) + $.get("#{url}").success(function(data) { + data.miq.zoomed = "#{opts[:zoomed]}"; + var chart = c3.generate(chartData(data.miqChart, data, { bindto: "##{chart_id}" })); + ManageIQ.charts.c3["#{chart_id}"] = chart; + miqSparkleOff(); + }); + EOJ + end + + def c3chart_local(data, opts = {}) + chart_id = opts[:id] || ('chart' + rand(10**8).to_s) + + content_tag(:div, '', :id => chart_id) + + javascript_tag(<<~EOJ) + var data = #{data.to_json}; + var chart = c3.generate(chartData('#{data[:miqChart]}', data, { bindto: "##{chart_id}" })); + ManageIQ.charts.c3["#{chart_id}"] = chart; + EOJ + end + end + end + end +end diff --git a/lib/manageiq/reporting/formatter/c3_series.rb b/lib/manageiq/reporting/formatter/c3_series.rb index a2f992630bc..b1f2cd5cd5f 100644 --- a/lib/manageiq/reporting/formatter/c3_series.rb +++ b/lib/manageiq/reporting/formatter/c3_series.rb @@ -1,27 +1,31 @@ -module ReportFormatter - class C3Series < Array - def initialize(*) - super() - end +module ManageIQ + module Reporting + module Formatter + class C3Series < Array + def initialize(*) + super() + end - def push(datum) - super(datum) - end + def push(datum) + super(datum) + end - def sum - super { |datum| datum[:value].to_f } - end + def sum + super { |datum| datum[:value].to_f } + end - def value_at(index) - self[index][:value] - end + def value_at(index) + self[index][:value] + end - def add_to_value(index, addition) - self[index][:value] += addition - end + def add_to_value(index, addition) + self[index][:value] += addition + end - def set_to_zero(index) - self[index][:value] = 0 + def set_to_zero(index) + self[index][:value] = 0 + end + end end end end diff --git a/lib/manageiq/reporting/formatter/chart_common.rb b/lib/manageiq/reporting/formatter/chart_common.rb index 3a6ea72b239..c653b5d3d0e 100644 --- a/lib/manageiq/reporting/formatter/chart_common.rb +++ b/lib/manageiq/reporting/formatter/chart_common.rb @@ -1,470 +1,474 @@ -module ReportFormatter - module ChartCommon - def slice_legend(string, limit = LEGEND_LENGTH) - string.to_s.gsub(/\n/, ' ').truncate(limit) - end - - def nonblank_or_default(value) - value.blank? ? BLANK_VALUE : value.to_s - end - - delegate :mri, :to => :options - - def build_document_header - raise "Can't create a graph without a sortby column" if mri.sortby.nil? && - mri.db != "MiqReport" # MiqReport based charts are already sorted - raise "Graph type not specified" if mri.graph.nil? || - (mri.graph.kind_of?(Hash) && mri.graph[:type].nil?) - end +module ManageIQ + module Reporting + module Formatter + module ChartCommon + def slice_legend(string, limit = LEGEND_LENGTH) + string.to_s.gsub(/\n/, ' ').truncate(limit) + end - delegate :graph_options, :to => :options + def nonblank_or_default(value) + value.blank? ? BLANK_VALUE : value.to_s + end - def build_document_body - return no_records_found_chart if mri.table.nil? || mri.table.data.blank? - maxcols = 8 - fun = case graph_options[:chart_type] - when :performance then :build_performance_chart # performance chart (time based) - when :util_ts then :build_util_ts_chart # utilization timestamp chart (grouped columns) - else # reporting charts - mri.graph[:mode] == 'values' ? :build_reporting_chart_numeric : :build_reporting_chart - end - method(fun).call(maxcols) - end + delegate :mri, :to => :options - def build_document_footer - end + def build_document_header + raise "Can't create a graph without a sortby column" if mri.sortby.nil? && + mri.db != "MiqReport" # MiqReport based charts are already sorted + raise "Graph type not specified" if mri.graph.nil? || + (mri.graph.kind_of?(Hash) && mri.graph[:type].nil?) + end - protected + delegate :graph_options, :to => :options + + def build_document_body + return no_records_found_chart if mri.table.nil? || mri.table.data.blank? + maxcols = 8 + fun = case graph_options[:chart_type] + when :performance then :build_performance_chart # performance chart (time based) + when :util_ts then :build_util_ts_chart # utilization timestamp chart (grouped columns) + else # reporting charts + mri.graph[:mode] == 'values' ? :build_reporting_chart_numeric : :build_reporting_chart + end + method(fun).call(maxcols) + end - # C&U performance charts (Cluster, Host, VM based) - def build_performance_chart_area(maxcols) - tz = mri.get_time_zone(Time.zone.name) + def build_document_footer + end - mri.graph[:columns].each_with_index do |col, col_idx| + protected + + # C&U performance charts (Cluster, Host, VM based) + def build_performance_chart_area(maxcols) + tz = mri.get_time_zone(Time.zone.name) + + mri.graph[:columns].each_with_index do |col, col_idx| + + next if col_idx >= maxcols + allnil = true + tip = graph_options[:trendtip] if col.starts_with?("trend") && graph_options[:trendtip] + categories = [] # Store categories and series counts in an array of arrays + series = series_class.new + mri.table.data.each_with_index do |r, d_idx| + rec_time = r["timestamp"].in_time_zone(tz) + + if mri.db.include?("Daily") || (mri.where_clause && mri.where_clause.include?("daily")) + categories.push(rec_time.month.to_s + "/" + rec_time.day.to_s) + elsif mri.extras[:realtime] == true + categories.push(rec_time.strftime("%H:%M:%S")) + else + categories.push(rec_time.hour.to_s + ":00") + end + val = r[col] + + if d_idx == mri.table.data.length - 1 && !tip.nil? + series.push(:value => val, :tooltip => tip) + else + series.push(:value => val) + end + allnil = false if !val.nil? + end + series.set_to_zero(-1) if allnil # XML/SWF Charts can't handle all nils, set the last value to 0 + add_axis_category_text(categories) - next if col_idx >= maxcols - allnil = true - tip = graph_options[:trendtip] if col.starts_with?("trend") && graph_options[:trendtip] - categories = [] # Store categories and series counts in an array of arrays - series = series_class.new - mri.table.data.each_with_index do |r, d_idx| - rec_time = r["timestamp"].in_time_zone(tz) + head = mri.graph[:legends] ? mri.graph[:legends][col_idx] : mri.headers[mri.col_order.index(col)] # Use legend overrides, if present - if mri.db.include?("Daily") || (mri.where_clause && mri.where_clause.include?("daily")) - categories.push(rec_time.month.to_s + "/" + rec_time.day.to_s) - elsif mri.extras[:realtime] == true - categories.push(rec_time.strftime("%H:%M:%S")) - else - categories.push(rec_time.hour.to_s + ":00") + add_series(head, series) end - val = r[col] + end - if d_idx == mri.table.data.length - 1 && !tip.nil? - series.push(:value => val, :tooltip => tip) - else - series.push(:value => val) - end - allnil = false if !val.nil? + def rounded_value(value) + return 0 if value.blank? + value.round(graph_options[:decimals] || 0) end - series.set_to_zero(-1) if allnil # XML/SWF Charts can't handle all nils, set the last value to 0 - add_axis_category_text(categories) - head = mri.graph[:legends] ? mri.graph[:legends][col_idx] : mri.headers[mri.col_order.index(col)] # Use legend overrides, if present + def build_performance_chart_pie(_maxcols) + col = mri.graph[:columns].first + mri.table.sort_rows_by!(col, :order => :descending) + categories = [] # Store categories and series counts in an array of arrays + series = series_class.new + cat_cnt = 0 + cat_total = mri.table.size + mri.table.data.each do |r| + cat = cat_cnt > 6 ? 'Others' : r["resource_name"] + val = rounded_value(r[col]) + next if val == 0 + if cat.starts_with?("Others") && categories[-1].starts_with?("Others") # Are we past the top 10? + categories[-1] = "Others" + series.add_to_value(-1, val) # Accumulate the series value + next + end + categories.push(cat) + cat_cnt += 1 + series.push(:value => val) + end - add_series(head, series) - end - end + return no_records_found_chart if series.empty? - def rounded_value(value) - return 0 if value.blank? - value.round(graph_options[:decimals] || 0) - end + add_axis_category_text(categories) + series.zip(categories) { |ser, category| ser[:tooltip] = category } + add_series('', series) + end - def build_performance_chart_pie(_maxcols) - col = mri.graph[:columns].first - mri.table.sort_rows_by!(col, :order => :descending) - categories = [] # Store categories and series counts in an array of arrays - series = series_class.new - cat_cnt = 0 - cat_total = mri.table.size - mri.table.data.each do |r| - cat = cat_cnt > 6 ? 'Others' : r["resource_name"] - val = rounded_value(r[col]) - next if val == 0 - if cat.starts_with?("Others") && categories[-1].starts_with?("Others") # Are we past the top 10? - categories[-1] = "Others" - series.add_to_value(-1, val) # Accumulate the series value - next + def format_bytes_human_size_1 + { + :function => { + :name => 'bytes_to_human_size', + :precision => 1 + } + } end - categories.push(cat) - cat_cnt += 1 - series.push(:value => val) - end - return no_records_found_chart if series.empty? + # Utilization timestamp charts + def build_util_ts_chart_column + categories = [] # Store categories and series counts in an array of arrays + series = [] + mri.graph[:columns].each_with_index do |col, col_idx| + mri.table.data.each do |r| + if col_idx == 0 # First column is the category text + categories.push(r[col]) + else + series[col_idx - 1] ||= {} + series[col_idx - 1][:header] ||= mri.headers[mri.col_order.index(col)] # Add the series header + series[col_idx - 1][:data] ||= series_class.new + tip_key = col + '_tip' + tip = case r[0] # Override the formatting for certain column groups on single day percent utilization chart + when "CPU" + mri.format(tip_key, r[tip_key], :format => { + :function => { + :name => "mhz_to_human_size", + :precision => "1" + }}) + when "Memory" + mri.format(tip_key, r[tip_key].to_f * 1024 * 1024, :format => format_bytes_human_size_1) + when "Disk" + mri.format(tip_key, r[tip_key], :format => format_bytes_human_size_1) + else + mri.format(tip_key, r[tip_key]) + end + series[col_idx - 1][:data].push( + :value => mri.format(col, r[col]).to_f, # ?? .to_f ?? + :tooltip => tip + ) + end + end + end - add_axis_category_text(categories) - series.zip(categories) { |ser, category| ser[:tooltip] = category } - add_series('', series) - end + # Remove categories (and associated series values) that have all zero or nil values + (categories.length - 1).downto(0) do |i| + sum = series.reduce(0.0) { |a, e| a + e[:data].value_at(i).to_f } + next if sum != 0 - def format_bytes_human_size_1 - { - :function => { - :name => 'bytes_to_human_size', - :precision => 1 - } - } - end + categories.delete_at(i) + series.each { |s| s[:data].delete_at(i) } # Remove the data for this cat across all series + end + + # Remove any series where all values are zero or nil + series.delete_if { |s| s[:data].sum == 0 } - # Utilization timestamp charts - def build_util_ts_chart_column - categories = [] # Store categories and series counts in an array of arrays - series = [] - mri.graph[:columns].each_with_index do |col, col_idx| - mri.table.data.each do |r| - if col_idx == 0 # First column is the category text - categories.push(r[col]) + if categories.empty? + no_records_found_chart("No data found for the selected day") + false else - series[col_idx - 1] ||= {} - series[col_idx - 1][:header] ||= mri.headers[mri.col_order.index(col)] # Add the series header - series[col_idx - 1][:data] ||= series_class.new - tip_key = col + '_tip' - tip = case r[0] # Override the formatting for certain column groups on single day percent utilization chart - when "CPU" - mri.format(tip_key, r[tip_key], :format => { - :function => { - :name => "mhz_to_human_size", - :precision => "1" - }}) - when "Memory" - mri.format(tip_key, r[tip_key].to_f * 1024 * 1024, :format => format_bytes_human_size_1) - when "Disk" - mri.format(tip_key, r[tip_key], :format => format_bytes_human_size_1) - else - mri.format(tip_key, r[tip_key]) - end - series[col_idx - 1][:data].push( - :value => mri.format(col, r[col]).to_f, # ?? .to_f ?? - :tooltip => tip - ) + add_axis_category_text(categories) + series.each { |s| add_series(s[:header], s[:data]) } + true end end - end - # Remove categories (and associated series values) that have all zero or nil values - (categories.length - 1).downto(0) do |i| - sum = series.reduce(0.0) { |a, e| a + e[:data].value_at(i).to_f } - next if sum != 0 + def keep_and_show_other + # Show other sum value by default + mri.graph.kind_of?(Hash) ? [mri.graph[:count].to_i, mri.graph[:other]] : [ReportController::Reports::Editor::GRAPH_MAX_COUNT, true] + end - categories.delete_at(i) - series.each { |s| s[:data].delete_at(i) } # Remove the data for this cat across all series - end + def build_reporting_chart_dim2 + (sort1, sort2) = mri.sortby + save1 = save2 = counter = save1_nonblank = save2_nonblank = nil + counts = {} # hash of hashes of counts + mri.table.data.each_with_index do |r, d_idx| + if d_idx == 0 || save1 != r[sort1].to_s + counts[save1_nonblank][save2_nonblank] = counter unless d_idx == 0 + save1 = r[sort1].to_s + save2 = r[sort2].to_s + save1_nonblank = nonblank_or_default(save1) + save2_nonblank = nonblank_or_default(save2) + counts[save1_nonblank] = Hash.new(0) + counter = 0 + else + if save2 != r[sort2].to_s # only the second sort field changed, save the count + counts[save1_nonblank][save2_nonblank] = counter + save2 = r[sort2].to_s + save2_nonblank = nonblank_or_default(save2) + counter = 0 + end + end + counter += 1 + end + # add the last key/value to the counts hash + counts[save1_nonblank][save2_nonblank] = counter + # We have all the counts, now we need to collect all of the . . . + sort1_vals = [] # sort field 1 values into an array and . . . + sort2_vals_counts = Hash.new(0) # sort field 2 values and counts into a Hash + counts.each do |key1, hash1| + sort1_vals.push(key1) + hash1.each { |key2, count2| sort2_vals_counts[key2] += count2 } + end + sort2_vals = sort2_vals_counts.sort { |a, b| b[1] <=> a[1] } # Sort the field values by count size descending - # Remove any series where all values are zero or nil - series.delete_if { |s| s[:data].sum == 0 } + # trim and add axis_category_text to the chart + sort1_vals.collect! { |value| slice_legend(value, LABEL_LENGTH) } + add_axis_category_text(sort1_vals) - if categories.empty? - no_records_found_chart("No data found for the selected day") - false - else - add_axis_category_text(categories) - series.each { |s| add_series(s[:header], s[:data]) } - true - end - end + # Now go through the counts hash again and put out a series for each sort field 1 hash of counts + (keep, show_other) = keep_and_show_other - def keep_and_show_other - # Show other sum value by default - mri.graph.kind_of?(Hash) ? [mri.graph[:count].to_i, mri.graph[:other]] : [ReportController::Reports::Editor::GRAPH_MAX_COUNT, true] - end + # If there are more than keep categories Keep the highest counts + other = keep < sort2_vals.length ? sort2_vals.slice!(keep..-1) : nil - def build_reporting_chart_dim2 - (sort1, sort2) = mri.sortby - save1 = save2 = counter = save1_nonblank = save2_nonblank = nil - counts = {} # hash of hashes of counts - mri.table.data.each_with_index do |r, d_idx| - if d_idx == 0 || save1 != r[sort1].to_s - counts[save1_nonblank][save2_nonblank] = counter unless d_idx == 0 - save1 = r[sort1].to_s - save2 = r[sort2].to_s - save1_nonblank = nonblank_or_default(save1) - save2_nonblank = nonblank_or_default(save2) - counts[save1_nonblank] = Hash.new(0) - counter = 0 - else - if save2 != r[sort2].to_s # only the second sort field changed, save the count - counts[save1_nonblank][save2_nonblank] = counter - save2 = r[sort2].to_s - save2_nonblank = nonblank_or_default(save2) - counter = 0 + sort2_vals.each do |val2| + series = counts.each_with_object(series_class.new) do |(key1, hash1), a| + a.push(:value => hash1[val2[0]], + :tooltip => "#{key1} / #{val2[0]}: #{hash1[val2[0]]}") + end + val2[0] = val2[0].to_s.gsub(/\\/, ' \ ') + add_series(val2[0].to_s, series) end - end - counter += 1 - end - # add the last key/value to the counts hash - counts[save1_nonblank][save2_nonblank] = counter - # We have all the counts, now we need to collect all of the . . . - sort1_vals = [] # sort field 1 values into an array and . . . - sort2_vals_counts = Hash.new(0) # sort field 2 values and counts into a Hash - counts.each do |key1, hash1| - sort1_vals.push(key1) - hash1.each { |key2, count2| sort2_vals_counts[key2] += count2 } - end - sort2_vals = sort2_vals_counts.sort { |a, b| b[1] <=> a[1] } # Sort the field values by count size descending - # trim and add axis_category_text to the chart - sort1_vals.collect! { |value| slice_legend(value, LABEL_LENGTH) } - add_axis_category_text(sort1_vals) - - # Now go through the counts hash again and put out a series for each sort field 1 hash of counts - (keep, show_other) = keep_and_show_other + if other.present? && show_other # Sum up the other sort2 counts by sort1 value + series = series_class.new + counts.each do |key1, hash1| # Go thru each sort1 key and hash count + # Add in all of the remaining sort2 key counts + ocount = other.reduce(0) { |a, e| a + hash1[e[0]] } + series.push(:value => ocount, + :tooltip => "#{key1} / Other: #{ocount}") + end + add_series(_("Other"), series) + end + counts + end - # If there are more than keep categories Keep the highest counts - other = keep < sort2_vals.length ? sort2_vals.slice!(keep..-1) : nil + def extract_column_names + # examples: + # 'Vm.hardware-cpu_sockets' gives 'hardware-cpu_sockets' + # 'Host-v_total_vms' gives 'v_total_vms' + # 'Vm-num_cpu:total' gives 'num_cpu' and 'num_cpu__total' + # "Vm::Providers::InfraManager::Vm-num_cpu:total" + # gives 'Vm::Providers::InfraManager::Vm' and 'num_cpu__total' + stage1, aggreg = mri.graph[:column].split(/(? hash1[val2[0]], - :tooltip => "#{key1} / #{val2[0]}: #{hash1[val2[0]]}") + def aggreg + extract_column_names unless @raw_column_name + @aggreg end - val2[0] = val2[0].to_s.gsub(/\\/, ' \ ') - add_series(val2[0].to_s, series) - end - if other.present? && show_other # Sum up the other sort2 counts by sort1 value - series = series_class.new - counts.each do |key1, hash1| # Go thru each sort1 key and hash count - # Add in all of the remaining sort2 key counts - ocount = other.reduce(0) { |a, e| a + hash1[e[0]] } - series.push(:value => ocount, - :tooltip => "#{key1} / Other: #{ocount}") + def raw_column_name + extract_column_names unless @raw_column_name + @raw_column_name end - add_series(_("Other"), series) - end - counts - end - def extract_column_names - # examples: - # 'Vm.hardware-cpu_sockets' gives 'hardware-cpu_sockets' - # 'Host-v_total_vms' gives 'v_total_vms' - # 'Vm-num_cpu:total' gives 'num_cpu' and 'num_cpu__total' - # "Vm::Providers::InfraManager::Vm-num_cpu:total" - # gives 'Vm::Providers::InfraManager::Vm' and 'num_cpu__total' - stage1, aggreg = mri.graph[:column].split(/(? row[data_column_name], + :tooltip => tooltip) + categories.push([tooltip, row[data_column_name]]) + end - def raw_column_name - extract_column_names unless @raw_column_name - @raw_column_name - end + if show_other + other_sum = Array(sorted_data[0, sorted_data.length - keep]) + .inject(0) { |sum, row| sum + (row[data_column_name] || 0) } + series.push(:value => other_sum, :tooltip => _('Other')) + categories.push([_('Other'), other_sum]) + end - def data_column_name - extract_column_names unless @data_column_name - @data_column_name - end + # Pie charts put categories in legend, else in axis labels + add_axis_category_text(categories) - # Options: - # sort1 -- labels - # data_column_name -- values - # - def build_numeric_chart_simple - categories = [] - (sort1,) = mri.sortby - (keep, show_other) = keep_and_show_other - sorted_data = mri.table.data.sort_by { |row| row[data_column_name] || 0 } - - series = sorted_data.reverse.take(keep) - .each_with_object(series_class.new(pie_type? ? :pie : :flat)) do |row, a| - tooltip = row[sort1] - tooltip = _('no value') if tooltip.blank? - a.push(:value => row[data_column_name], - :tooltip => tooltip) - categories.push([tooltip, row[data_column_name]]) - end + add_series(chart_is_2d? ? mri.chart_header_column : nil, series) + end - if show_other - other_sum = Array(sorted_data[0, sorted_data.length - keep]) - .inject(0) { |sum, row| sum + (row[data_column_name] || 0) } - series.push(:value => other_sum, :tooltip => _('Other')) - categories.push([_('Other'), other_sum]) - end + def build_numeric_chart_grouped + (keep, show_other) = keep_and_show_other + show_other &&= (aggreg == :total) # FIXME: we only support :total + + groups = mri.build_subtotals.reject { |k, _| k == :_total_ } + sorted_data = groups.sort_by { |_, data| data[aggreg][raw_column_name] || 0 } + + categories = [] + series = sorted_data.reverse.take(keep) + .each_with_object(series_class.new(pie_type? ? :pie : :flat)) do |(key, data), a| + tooltip = key + tooltip = _('no value') if key.blank? + a.push(:value => data[aggreg][raw_column_name], + :tooltip => tooltip) + categories.push([tooltip, data[aggreg][raw_column_name]]) + end - # Pie charts put categories in legend, else in axis labels - add_axis_category_text(categories) + if show_other + other_sum = Array(sorted_data[0, sorted_data.length - keep]) + .inject(0) { |sum, (_key, row)| sum + row[aggreg][raw_column_name] } - add_series(chart_is_2d? ? mri.chart_header_column : nil, series) - end + series.push(:value => other_sum, :tooltip => _('Other')) + categories.push([_('Other'), other_sum]) + end - def build_numeric_chart_grouped - (keep, show_other) = keep_and_show_other - show_other &&= (aggreg == :total) # FIXME: we only support :total - - groups = mri.build_subtotals.reject { |k, _| k == :_total_ } - sorted_data = groups.sort_by { |_, data| data[aggreg][raw_column_name] || 0 } - - categories = [] - series = sorted_data.reverse.take(keep) - .each_with_object(series_class.new(pie_type? ? :pie : :flat)) do |(key, data), a| - tooltip = key - tooltip = _('no value') if key.blank? - a.push(:value => data[aggreg][raw_column_name], - :tooltip => tooltip) - categories.push([tooltip, data[aggreg][raw_column_name]]) - end + # Pie charts put categories in legend, else in axis labels + add_axis_category_text(categories) - if show_other - other_sum = Array(sorted_data[0, sorted_data.length - keep]) - .inject(0) { |sum, (_key, row)| sum + row[aggreg][raw_column_name] } + add_series(chart_is_2d? ? mri.chart_header_column : nil, series) + end - series.push(:value => other_sum, :tooltip => _('Other')) - categories.push([_('Other'), other_sum]) - end + def build_numeric_chart_grouped_2dim + (sort1, sort2) = mri.sortby + (keep, show_other) = keep_and_show_other + show_other &&= (aggreg == :total) # FIXME: we only support :total - # Pie charts put categories in legend, else in axis labels - add_axis_category_text(categories) + subtotals = mri.build_subtotals(true).reject { |k, _| k == :_total_ } - add_series(chart_is_2d? ? mri.chart_header_column : nil, series) - end + # Group values by sort1 + # 3rd dimension in the chart is defined by sort2 + groups = mri.table.data.group_by { |row| row[sort1] } - def build_numeric_chart_grouped_2dim - (sort1, sort2) = mri.sortby - (keep, show_other) = keep_and_show_other - show_other &&= (aggreg == :total) # FIXME: we only support :total + def_range_key2 = subtotals.keys.map { |key| key.split('__')[1] || '' }.sort.uniq - subtotals = mri.build_subtotals(true).reject { |k, _| k == :_total_ } + group_sums = groups.keys.each_with_object({}) do |key1, h| + h[key1] = def_range_key2.inject(0) do |sum, key2| + sub_key = "#{key1}__#{key2}" + subtotals.key?(sub_key) ? sum + subtotals[sub_key][aggreg][raw_column_name] : sum + end + end - # Group values by sort1 - # 3rd dimension in the chart is defined by sort2 - groups = mri.table.data.group_by { |row| row[sort1] } + sorted_sums = group_sums.sort_by { |_key, sum| sum } - def_range_key2 = subtotals.keys.map { |key| key.split('__')[1] || '' }.sort.uniq + selected_groups = sorted_sums.reverse.take(keep) - group_sums = groups.keys.each_with_object({}) do |key1, h| - h[key1] = def_range_key2.inject(0) do |sum, key2| - sub_key = "#{key1}__#{key2}" - subtotals.key?(sub_key) ? sum + subtotals[sub_key][aggreg][raw_column_name] : sum - end - end + cathegory_texts = selected_groups.collect do |key, _| + label = key + label = _('no value') if label.blank? + label + end + cathegory_texts << _('Other') if show_other - sorted_sums = group_sums.sort_by { |_key, sum| sum } + add_axis_category_text(cathegory_texts) - selected_groups = sorted_sums.reverse.take(keep) + if show_other + other_groups = Array(sorted_sums[0, sorted_sums.length - keep]) + other = other_groups.each_with_object(Hash.new(0)) do |(key, _), o| + groups[key].each { |row| o[row[sort2]] += row[raw_column_name] } + end + end - cathegory_texts = selected_groups.collect do |key, _| - label = key - label = _('no value') if label.blank? - label - end - cathegory_texts << _('Other') if show_other + # For each value in sort2 column we create a series. + sort2_values = mri.table.data.map { |row| row[sort2] }.uniq + sort2_values.each do |val2| + series = selected_groups.each_with_object(series_class.new) do |(key1, _), a| + sub_key = "#{key1}__#{val2}" + value = subtotals.key?(sub_key) ? subtotals[sub_key][aggreg][raw_column_name] : 0 - add_axis_category_text(cathegory_texts) + a.push(:value => value, + :tooltip => "#{key1} / #{val2}: #{value}") + end - if show_other - other_groups = Array(sorted_sums[0, sorted_sums.length - keep]) - other = other_groups.each_with_object(Hash.new(0)) do |(key, _), o| - groups[key].each { |row| o[row[sort2]] += row[raw_column_name] } + series.push(:value => other[val2], + :tooltip => "Other / #{val2}: #{other[val2]}") if show_other + label = val2 if val2.kind_of?(String) + label = label.to_s.gsub(/\\/, ' \ ') + label = _('no value') if label.blank? + add_series(label, series) + end + groups.keys.collect { |k| k.blank? ? _('no value') : k } end - end - - # For each value in sort2 column we create a series. - sort2_values = mri.table.data.map { |row| row[sort2] }.uniq - sort2_values.each do |val2| - series = selected_groups.each_with_object(series_class.new) do |(key1, _), a| - sub_key = "#{key1}__#{val2}" - value = subtotals.key?(sub_key) ? subtotals[sub_key][aggreg][raw_column_name] : 0 - a.push(:value => value, - :tooltip => "#{key1} / #{val2}: #{value}") + def pie_type? + @pie_type ||= mri.graph[:type] =~ /^(Pie|Donut)/ end - series.push(:value => other[val2], - :tooltip => "Other / #{val2}: #{other[val2]}") if show_other - label = val2 if val2.kind_of?(String) - label = label.to_s.gsub(/\\/, ' \ ') - label = _('no value') if label.blank? - add_series(label, series) - end - groups.keys.collect { |k| k.blank? ? _('no value') : k } - end - - def pie_type? - @pie_type ||= mri.graph[:type] =~ /^(Pie|Donut)/ - end + def build_reporting_chart_other + save_key = nil + counter = 0 + categories = [] # Store categories and series counts in an array of arrays + mri.table.data.each_with_index do |r, d_idx| + category_changed = save_key != r[mri.sortby[0]] + not_first_iteration = d_idx > 0 + if not_first_iteration && category_changed + categories.push([save_key, counter]) # Push current category and count onto the array + counter = 0 + end + save_key = r[mri.sortby[0]] + counter += 1 + end + categories.push([save_key, counter]) # Push last category and count onto the array + + (keep, show_other) = keep_and_show_other + kept_categories = categories + kept_categories.reject! { |a| a.first.nil? } + kept_categories = kept_categories.sort_by(&:first).take(keep) + kept_categories.reverse! if mri.order == "Descending" + kept_categories.push(["Other", (categories - kept_categories).reduce(0) { |a, e| a + e.last }]) if show_other + kept_categories.map { |cat| [nonblank_or_default(cat.first), cat.last] } + + series = kept_categories.each_with_object( + series_class.new(pie_type? ? :pie : :flat)) do |cat, a| + a.push(:value => cat.last, :tooltip => "#{cat.first}: #{cat.last}") + end - def build_reporting_chart_other - save_key = nil - counter = 0 - categories = [] # Store categories and series counts in an array of arrays - mri.table.data.each_with_index do |r, d_idx| - category_changed = save_key != r[mri.sortby[0]] - not_first_iteration = d_idx > 0 - if not_first_iteration && category_changed - categories.push([save_key, counter]) # Push current category and count onto the array - counter = 0 + # Pie charts put categories in legend, else in axis labels + add_axis_category_text(kept_categories) + add_series(chart_is_2d? ? mri.chart_header_column : nil, series) end - save_key = r[mri.sortby[0]] - counter += 1 - end - categories.push([save_key, counter]) # Push last category and count onto the array - - (keep, show_other) = keep_and_show_other - kept_categories = categories - kept_categories.reject! { |a| a.first.nil? } - kept_categories = kept_categories.sort_by(&:first).take(keep) - kept_categories.reverse! if mri.order == "Descending" - kept_categories.push(["Other", (categories - kept_categories).reduce(0) { |a, e| a + e.last }]) if show_other - kept_categories.map { |cat| [nonblank_or_default(cat.first), cat.last] } - - series = kept_categories.each_with_object( - series_class.new(pie_type? ? :pie : :flat)) do |cat, a| - a.push(:value => cat.last, :tooltip => "#{cat.first}: #{cat.last}") - end - # Pie charts put categories in legend, else in axis labels - add_axis_category_text(kept_categories) - add_series(chart_is_2d? ? mri.chart_header_column : nil, series) - end + # C&U performance charts (Cluster, Host, VM based) + def build_performance_chart(maxcols) + case mri.graph[:type] + when "Area", "AreaThreed", "Line", "StackedArea", + "StackedThreedArea", "ParallelThreedColumn" + build_performance_chart_area(maxcols) + when "Pie", "PieThreed" + build_performance_chart_pie(maxcols) + end + end - # C&U performance charts (Cluster, Host, VM based) - def build_performance_chart(maxcols) - case mri.graph[:type] - when "Area", "AreaThreed", "Line", "StackedArea", - "StackedThreedArea", "ParallelThreedColumn" - build_performance_chart_area(maxcols) - when "Pie", "PieThreed" - build_performance_chart_pie(maxcols) - end - end + # Utilization timestamp charts + def build_util_ts_chart(_maxcols) + build_util_ts_chart_column if %w(Column ColumnThreed).index(mri.graph[:type]) + end - # Utilization timestamp charts - def build_util_ts_chart(_maxcols) - build_util_ts_chart_column if %w(Column ColumnThreed).index(mri.graph[:type]) - end + def build_reporting_chart_numeric(_maxcols) + return no_records_found_chart(_('Invalid chart definition')) unless mri.graph[:column].present? + if mri.group.nil? + build_numeric_chart_simple + else + mri.dims == 2 ? build_numeric_chart_grouped_2dim : build_numeric_chart_grouped + end + end - def build_reporting_chart_numeric(_maxcols) - return no_records_found_chart(_('Invalid chart definition')) unless mri.graph[:column].present? - if mri.group.nil? - build_numeric_chart_simple - else - mri.dims == 2 ? build_numeric_chart_grouped_2dim : build_numeric_chart_grouped + def build_reporting_chart(_maxcols) + mri.dims == 2 ? build_reporting_chart_dim2 : build_reporting_chart_other + end end end - - def build_reporting_chart(_maxcols) - mri.dims == 2 ? build_reporting_chart_dim2 : build_reporting_chart_other - end end end diff --git a/lib/manageiq/reporting/formatter/converter.rb b/lib/manageiq/reporting/formatter/converter.rb index dcac8fed6e1..e57f7e3ba4b 100644 --- a/lib/manageiq/reporting/formatter/converter.rb +++ b/lib/manageiq/reporting/formatter/converter.rb @@ -1,19 +1,23 @@ -module ReportFormatter - class Converter - # generate a ruport table from an array of hashes where the keys are the column names - def self.hashes2table(hashes, options) - return Ruport::Data::Table.new if hashes.blank? +module ManageIQ + module Reporting + module Formatter + class Converter + # generate a ruport table from an array of hashes where the keys are the column names + def self.hashes2table(hashes, options) + return Ruport::Data::Table.new if hashes.blank? - data = hashes.inject([]) do |arr, h| - nh = {} - options[:only].each { |col| nh[col] = h[col] } - arr << nh - end + data = hashes.inject([]) do |arr, h| + nh = {} + options[:only].each { |col| nh[col] = h[col] } + arr << nh + end - data = data[0..options[:limit] - 1] if options[:limit] # apply limit - Ruport::Data::Table.new(:data => data, - :column_names => options[:only], - :filters => options[:filters]) + data = data[0..options[:limit] - 1] if options[:limit] # apply limit + Ruport::Data::Table.new(:data => data, + :column_names => options[:only], + :filters => options[:filters]) + end + end end end end diff --git a/lib/manageiq/reporting/formatter/html.rb b/lib/manageiq/reporting/formatter/html.rb index 07f10a1a88d..292854689bb 100644 --- a/lib/manageiq/reporting/formatter/html.rb +++ b/lib/manageiq/reporting/formatter/html.rb @@ -1,65 +1,69 @@ -module ReportFormatter - class ReportHTML < Ruport::Formatter - renders :html, :for => ReportRenderer +module ManageIQ + module Reporting + module Formatter + class HTML < Ruport::Formatter + renders :html, :for => ManageIQ::Reporting::Formatter::ReportRenderer - def build_html_title - mri = options.mri - mri.html_title = '' - mri.html_title << "
" - mri.html_title << "" - mri.html_title << '
' - mri.html_title << '
' - end + def build_html_title + mri = options.mri + mri.html_title = '' + mri.html_title << "
" + mri.html_title << "" + mri.html_title << '
' + mri.html_title << '
' + end - def pad(str, len) - return "".ljust(len) if str.nil? - str = str.slice(0, len) # truncate long strings - str.ljust(len) # pad with whitespace - end + def pad(str, len) + return "".ljust(len) if str.nil? + str = str.slice(0, len) # truncate long strings + str.ljust(len) # pad with whitespace + end - def build_document_header - build_html_title - end + def build_document_header + build_html_title + end - def build_document_body - mri = options.mri - output << "" - output << "" - output << "" + def build_document_body + mri = options.mri + output << "
" + output << "" + output << "" - # table heading - unless mri.headers.nil? - mri.headers.each do |h| - output << "" + # table heading + unless mri.headers.nil? + mri.headers.each do |h| + output << "" + end + output << "" + output << "" + end + output << '' + output << mri.build_html_rows.join + output << '' end - output << "" - output << "" - end - output << '' - output << mri.build_html_rows.join - output << '' - end - def build_document_footer - mri = options.mri - output << "" - output << "" - output << "" - output << "
" << CGI.escapeHTML(h.to_s) << "" << CGI.escapeHTML(h.to_s) << "
" - output << "" - output << "" - output << "
" + def build_document_footer + mri = options.mri + output << "" + output << "" + output << "" + output << "" + output << "" + output << "" + output << "" - if mri.filter_summary - output << mri.filter_summary.to_s - end - end + if mri.filter_summary + output << mri.filter_summary.to_s + end + end - def finalize_document - output + def finalize_document + output + end + end end end end diff --git a/lib/manageiq/reporting/formatter/report_renderer.rb b/lib/manageiq/reporting/formatter/report_renderer.rb index 3964532d340..1f1eedecee7 100644 --- a/lib/manageiq/reporting/formatter/report_renderer.rb +++ b/lib/manageiq/reporting/formatter/report_renderer.rb @@ -1,7 +1,11 @@ -module ReportFormatter - class ReportRenderer < Ruport::Controller - stage :document_header, :document_body, :document_footer - finalize :document - options { |o| o.mri = o.show_title = o.theme = o.table_width = o.alignment = o.graph_options = nil } +module ManageIQ + module Reporting + module Formatter + class ReportRenderer < Ruport::Controller + stage :document_header, :document_body, :document_footer + finalize :document + options { |o| o.mri = o.show_title = o.theme = o.table_width = o.alignment = o.graph_options = nil } + end + end end end diff --git a/lib/manageiq/reporting/formatter/text.rb b/lib/manageiq/reporting/formatter/text.rb index a8faaf6ec18..c1acad79bd9 100644 --- a/lib/manageiq/reporting/formatter/text.rb +++ b/lib/manageiq/reporting/formatter/text.rb @@ -1,262 +1,266 @@ -module ReportFormatter - class ReportText < Ruport::Formatter::Text - renders :text, :for => ReportRenderer +module ManageIQ + module Reporting + module Formatter + class Text < Ruport::Formatter::Text + renders :text, :for => ReportRenderer - # determines the text widths for each column. - def calculate_max_col_widths - mri = options.mri - tz = mri.get_time_zone(Time.zone.name) + # determines the text widths for each column. + def calculate_max_col_widths + mri = options.mri + tz = mri.get_time_zone(Time.zone.name) - @max_col_width = [] - unless mri.headers.empty? - mri.headers.each_index do |i| - @max_col_width[i] = mri.headers[i].to_s.length - end - end - mri.table.data.each do |r| - mri.col_formats ||= [] # Backward compat - create empty array for formats - mri.col_order.each_with_index do |f, i| - unless ["", ""].include?(mri.db) - data = mri.format(f, - r[f], - :format => mri.col_formats[i] ? mri.col_formats[i] : :_default_, - :tz => tz) - else - data = r[f].to_s + @max_col_width = [] + unless mri.headers.empty? + mri.headers.each_index do |i| + @max_col_width[i] = mri.headers[i].to_s.length + end end - if !@max_col_width[i] || data.length > @max_col_width[i] - @max_col_width[i] = data.length + mri.table.data.each do |r| + mri.col_formats ||= [] # Backward compat - create empty array for formats + mri.col_order.each_with_index do |f, i| + unless ["", ""].include?(mri.db) + data = mri.format(f, + r[f], + :format => mri.col_formats[i] ? mri.col_formats[i] : :_default_, + :tz => tz) + else + data = r[f].to_s + end + if !@max_col_width[i] || data.length > @max_col_width[i] + @max_col_width[i] = data.length + end + end end end - end - end - # method to get friendly values for company tag and user filters - def calculate_filter_names(tag) - categories = Classification.categories.collect { |c| c if c.show }.compact - tag_val = "" - categories.each do |category| - entries = {} - category.entries.each do |entry| - entries[entry.description] = entry.tag.name # Get the fully qual tag name - if tag == entry.tag.name - tag_val = "#{category.description}: #{entry.description}" + # method to get friendly values for company tag and user filters + def calculate_filter_names(tag) + categories = Classification.categories.collect { |c| c if c.show }.compact + tag_val = "" + categories.each do |category| + entries = {} + category.entries.each do |entry| + entries[entry.description] = entry.tag.name # Get the fully qual tag name + if tag == entry.tag.name + tag_val = "#{category.description}: #{entry.description}" + end + end end + tag_val end - end - tag_val - end - # Uses the column names from the given Data::Table to generate a table - # header. - # - # calls fit_to_width to truncate table heading if necessary. - def build_document_header - mri = options.mri - raise "No settings configured for Table" if mri.table.nil? - calculate_max_col_widths - @hr = hr + # Uses the column names from the given Data::Table to generate a table + # header. + # + # calls fit_to_width to truncate table heading if necessary. + def build_document_header + mri = options.mri + raise "No settings configured for Table" if mri.table.nil? + calculate_max_col_widths + @hr = hr + + unless mri.title.nil? # generate title line, if present + output << fit_to_width(@hr) - unless mri.title.nil? # generate title line, if present - output << fit_to_width(@hr) + temp_title = mri.title + temp_title << " (" << mri.report_run_time.to_s << ")" unless mri.report_run_time.nil? + t = temp_title.center(@line_len - 2) + output << fit_to_width("|#{t}|" + CRLF) + if !mri.db.nil? && mri.db == "" + t2 = "(* = Value changed from previous column)" + t2 = t2.center(@line_len - 2) + output << fit_to_width("|#{t2}|" + CRLF) + end + end - temp_title = mri.title - temp_title << " (" << mri.report_run_time.to_s << ")" unless mri.report_run_time.nil? - t = temp_title.center(@line_len - 2) - output << fit_to_width("|#{t}|" + CRLF) - if !mri.db.nil? && mri.db == "" - t2 = "(* = Value changed from previous column)" - t2 = t2.center(@line_len - 2) - output << fit_to_width("|#{t2}|" + CRLF) + return if mri.headers.empty? + c = mri.headers.dup + # Remove headers of hidden columns + mri.col_order.each_with_index do |f, i| + c.delete_at(i) if mri.column_is_hidden?(f) + end + c.each_with_index do |f, i| + c[i] = f.to_s.center(@max_col_width[i]) + end + output << fit_to_width("#{@hr}| #{Array(c).join(' | ')} |" + CRLF) end - end - return if mri.headers.empty? - c = mri.headers.dup - # Remove headers of hidden columns - mri.col_order.each_with_index do |f, i| - c.delete_at(i) if mri.column_is_hidden?(f) - end - c.each_with_index do |f, i| - c[i] = f.to_s.center(@max_col_width[i]) - end - output << fit_to_width("#{@hr}| #{Array(c).join(' | ')} |" + CRLF) - end + # Generates the body of the text table. + # + # Defaults to numeric values being right justified, and other values being + # left justified. Can be changed to support centering of output by + # setting alignment to :center + # + # Uses fit_to_width to truncate table if necessary + def build_document_body + mri = options.mri + tz = mri.get_time_zone(Time.zone.name) + s = @hr - # Generates the body of the text table. - # - # Defaults to numeric values being right justified, and other values being - # left justified. Can be changed to support centering of output by - # setting alignment to :center - # - # Uses fit_to_width to truncate table if necessary - def build_document_body - mri = options.mri - tz = mri.get_time_zone(Time.zone.name) - s = @hr + save_val = nil + counter = 0 - save_val = nil - counter = 0 + row_limit = mri.rpt_options && mri.rpt_options[:row_limit] ? mri.rpt_options[:row_limit] : 0 + use_table = mri.sub_table ? mri.sub_table : mri.table + use_table.data.each_with_index do |r, d_idx| + break if row_limit != 0 && d_idx > row_limit - 1 + line = [] + line_wrapper = false # Clear line wrapper flag + if [""].include?(mri.db) && r[0] == "% Match:" + line_wrapper = true # Wrap compare % lines with header rows + elsif [""].include?(mri.db) && r[0] == "Changed:" + line_wrapper = true # Wrap drift changed lines with header rows + end + mri.col_formats ||= [] # Backward compat - create empty array for formats + mri.col_order.each_with_index do |f, i| + next if mri.column_is_hidden?(f) - row_limit = mri.rpt_options && mri.rpt_options[:row_limit] ? mri.rpt_options[:row_limit] : 0 - use_table = mri.sub_table ? mri.sub_table : mri.table - use_table.data.each_with_index do |r, d_idx| - break if row_limit != 0 && d_idx > row_limit - 1 - line = [] - line_wrapper = false # Clear line wrapper flag - if [""].include?(mri.db) && r[0] == "% Match:" - line_wrapper = true # Wrap compare % lines with header rows - elsif [""].include?(mri.db) && r[0] == "Changed:" - line_wrapper = true # Wrap drift changed lines with header rows - end - mri.col_formats ||= [] # Backward compat - create empty array for formats - mri.col_order.each_with_index do |f, i| - next if mri.column_is_hidden?(f) + unless ["", ""].include?(mri.db) + data = mri.format(f, + r[f], + :format => mri.col_formats[i] ? mri.col_formats[i] : :_default_, + :tz => tz) + else + data = r[f].to_s + end + if options.alignment.eql? :center + line << data.center(@max_col_width[i]) + else + align = data.kind_of?(Numeric) ? :rjust : :ljust + line << data.send(align, @max_col_width[i]) + end + end - unless ["", ""].include?(mri.db) - data = mri.format(f, - r[f], - :format => mri.col_formats[i] ? mri.col_formats[i] : :_default_, - :tz => tz) - else - data = r[f].to_s - end - if options.alignment.eql? :center - line << data.center(@max_col_width[i]) - else - align = data.kind_of?(Numeric) ? :rjust : :ljust - line << data.send(align, @max_col_width[i]) + # generate a break line if grouping is turned on + if ["y", "c"].include?(mri.group) && !mri.sortby.nil? + if d_idx > 0 && save_val != r[mri.sortby[0]] + if mri.group == "c" + s += @hr + t = " Total for #{save_val}: #{counter} ".center(@line_len - 2) + s += fit_to_width("|#{t}|" + CRLF) + s += @hr + counter = 0 + else + s += @hr + end + end + save_val = r[mri.sortby[0]] + counter += 1 + end + s += @hr if line_wrapper + s += "| #{line.join(' | ')} |" + CRLF + s += @hr if line_wrapper end - end - # generate a break line if grouping is turned on - if ["y", "c"].include?(mri.group) && !mri.sortby.nil? - if d_idx > 0 && save_val != r[mri.sortby[0]] + # see if a final group line needs to be written + if ["y", "c"].include?(mri.group) && !mri.sortby.nil? if mri.group == "c" s += @hr t = " Total for #{save_val}: #{counter} ".center(@line_len - 2) s += fit_to_width("|#{t}|" + CRLF) - s += @hr - counter = 0 - else - s += @hr end end - save_val = r[mri.sortby[0]] - counter += 1 - end - s += @hr if line_wrapper - s += "| #{line.join(' | ')} |" + CRLF - s += @hr if line_wrapper - end - # see if a final group line needs to be written - if ["y", "c"].include?(mri.group) && !mri.sortby.nil? - if mri.group == "c" s += @hr - t = " Total for #{save_val}: #{counter} ".center(@line_len - 2) - s += fit_to_width("|#{t}|" + CRLF) + output << fit_to_width(s) end - end - - s += @hr - output << fit_to_width(s) - end - def build_document_footer - mri = options.mri - tz = mri.get_time_zone(Time.zone.name) - if !mri.user_categories.blank? || !mri.categories.blank? || !mri.conditions.nil? || !mri.display_filter.nil? - output << fit_to_width(@hr) - unless mri.user_categories.blank? - user_filters = mri.user_categories.flatten - unless user_filters.blank? - customer_name = Tenant.root_tenant.name - user_filter = "User assigned " + customer_name + " Tag filters:" - t = user_filter.ljust(@line_len - 2) - output << fit_to_width("|#{t}|" + CRLF) - user_filters.each do |filters| - tag_val = " " + calculate_filter_names(filters) - tag_val1 = tag_val + " " * (@line_len - tag_val.length - 2) - output << fit_to_width("|#{tag_val1}|" + CRLF) + def build_document_footer + mri = options.mri + tz = mri.get_time_zone(Time.zone.name) + if !mri.user_categories.blank? || !mri.categories.blank? || !mri.conditions.nil? || !mri.display_filter.nil? + output << fit_to_width(@hr) + unless mri.user_categories.blank? + user_filters = mri.user_categories.flatten + unless user_filters.blank? + customer_name = Tenant.root_tenant.name + user_filter = "User assigned " + customer_name + " Tag filters:" + t = user_filter.ljust(@line_len - 2) + output << fit_to_width("|#{t}|" + CRLF) + user_filters.each do |filters| + tag_val = " " + calculate_filter_names(filters) + tag_val1 = tag_val + " " * (@line_len - tag_val.length - 2) + output << fit_to_width("|#{tag_val1}|" + CRLF) + end + end end - end - end - unless mri.categories.blank? - categories = mri.categories.flatten - unless categories.blank? - customer_name = Tenant.root_tenant.name - customer_name_title = "Report based " + customer_name + " Tag filters:" - t = customer_name_title + " " * (@line_len - customer_name_title.length - 2) - output << fit_to_width("|#{t}|" + CRLF) - categories.each do |filters| - tag_val = " " + calculate_filter_names(filters) - tag_val1 = tag_val + " " * (@line_len - tag_val.length - 2) - output << fit_to_width("|#{tag_val1}|" + CRLF) + unless mri.categories.blank? + categories = mri.categories.flatten + unless categories.blank? + customer_name = Tenant.root_tenant.name + customer_name_title = "Report based " + customer_name + " Tag filters:" + t = customer_name_title + " " * (@line_len - customer_name_title.length - 2) + output << fit_to_width("|#{t}|" + CRLF) + categories.each do |filters| + tag_val = " " + calculate_filter_names(filters) + tag_val1 = tag_val + " " * (@line_len - tag_val.length - 2) + output << fit_to_width("|#{tag_val1}|" + CRLF) + end + end end - end - end - unless mri.conditions.nil? - if mri.conditions.kind_of?(Hash) - filter_fields = "Report based filter fields:" - t = filter_fields.ljust(@line_len - 2) - output << fit_to_width("|#{t}|" + CRLF) + unless mri.conditions.nil? + if mri.conditions.kind_of?(Hash) + filter_fields = "Report based filter fields:" + t = filter_fields.ljust(@line_len - 2) + output << fit_to_width("|#{t}|" + CRLF) - # Clean up the conditions for display - tables = mri.conditions[:field].split("-")[0].split(".") # Get the model and tables - field = Dictionary.gettext(tables[0], :type => :model, :notfound => :titleize) # Start with the model - tables[1..-1].each do |t| # Add on any tables - field += "." + Dictionary.gettext(t, :type => :table, :notfound => :titleize) + # Clean up the conditions for display + tables = mri.conditions[:field].split("-")[0].split(".") # Get the model and tables + field = Dictionary.gettext(tables[0], :type => :model, :notfound => :titleize) # Start with the model + tables[1..-1].each do |t| # Add on any tables + field += "." + Dictionary.gettext(t, :type => :table, :notfound => :titleize) + end + # Add on the column name + field += " : " + Dictionary.gettext(mri.conditions[:field].split("-")[1], :type => :column, :notfound => :titleize) + + filter_val = " " + field + " " + mri.conditions[:operator] + " " + mri.conditions[:string].to_s + t = filter_val + " " * (@line_len - filter_val.length - 2) + output << fit_to_width("|#{t}|" + CRLF) + else + filter_fields = "Report based filter fields:" + t = filter_fields.ljust(@line_len - 2) + output << fit_to_width("|#{t}|" + CRLF) + filter_val = mri.conditions.to_human + t = filter_val.ljust(@line_len - 2) + output << fit_to_width("|#{t}|" + CRLF) + end end - # Add on the column name - field += " : " + Dictionary.gettext(mri.conditions[:field].split("-")[1], :type => :column, :notfound => :titleize) - filter_val = " " + field + " " + mri.conditions[:operator] + " " + mri.conditions[:string].to_s - t = filter_val + " " * (@line_len - filter_val.length - 2) - output << fit_to_width("|#{t}|" + CRLF) - else - filter_fields = "Report based filter fields:" - t = filter_fields.ljust(@line_len - 2) - output << fit_to_width("|#{t}|" + CRLF) - filter_val = mri.conditions.to_human - t = filter_val.ljust(@line_len - 2) - output << fit_to_width("|#{t}|" + CRLF) + unless mri.display_filter.nil? + filter_fields = "Display Filter:" + t = filter_fields.ljust(@line_len - 2) + output << fit_to_width("|#{t}|" + CRLF) + filter_val = mri.display_filter.to_human + t = filter_val + " " * (@line_len - filter_val.length - 2) + output << fit_to_width("|#{t}|" + CRLF) + end end - end - unless mri.display_filter.nil? - filter_fields = "Display Filter:" - t = filter_fields.ljust(@line_len - 2) - output << fit_to_width("|#{t}|" + CRLF) - filter_val = mri.display_filter.to_human - t = filter_val + " " * (@line_len - filter_val.length - 2) - output << fit_to_width("|#{t}|" + CRLF) + output << fit_to_width(@hr) + # Label footer with last run on time of selected report or current time for other downloads + last_run_on = mri.rpt_options && mri.rpt_options[:last_run_on] || Time.zone.now + cr = format_timezone(last_run_on, tz).to_s + f = cr.center(@line_len - 2) + output << fit_to_width("|#{f}|" + CRLF) + output << fit_to_width(@hr) end - end - - output << fit_to_width(@hr) - # Label footer with last run on time of selected report or current time for other downloads - last_run_on = mri.rpt_options && mri.rpt_options[:last_run_on] || Time.zone.now - cr = format_timezone(last_run_on, tz).to_s - f = cr.center(@line_len - 2) - output << fit_to_width("|#{f}|" + CRLF) - output << fit_to_width(@hr) - end - # Generates the horizontal rule by calculating the total table width and - # then generating a bar that looks like this: - # - # "+------------------+" - def hr - columns = options.mri.table.column_names - if columns.include?("id") # Use 1 less column if "id" is present - @line_len = @max_col_width.inject((columns.length - 1) * 3) { |s, e| s + e } + 1 - else - @line_len = @max_col_width.inject(columns.length * 3) { |s, e| s + e } + # Generates the horizontal rule by calculating the total table width and + # then generating a bar that looks like this: + # + # "+------------------+" + def hr + columns = options.mri.table.column_names + if columns.include?("id") # Use 1 less column if "id" is present + @line_len = @max_col_width.inject((columns.length - 1) * 3) { |s, e| s + e } + 1 + else + @line_len = @max_col_width.inject(columns.length * 3) { |s, e| s + e } + end + "+" + "-" * (@line_len - 2) + "+" + CRLF + end end - "+" + "-" * (@line_len - 2) + "+" + CRLF end end end diff --git a/lib/manageiq/reporting/formatter/timeline.rb b/lib/manageiq/reporting/formatter/timeline.rb index a25ba307dca..a42de8341f6 100644 --- a/lib/manageiq/reporting/formatter/timeline.rb +++ b/lib/manageiq/reporting/formatter/timeline.rb @@ -1,181 +1,187 @@ # Timeline formatter - creates Timeline XML stream to feed Simile Timelines -module ReportFormatter - class ReportTimeline < Ruport::Formatter - renders :timeline, :for => ReportRenderer +require_dependency 'manageiq/reporting/formatter/timeline_message' - # create the graph object and add titles, fonts, and colors - def build_document_header - mri = options.mri - raise "No settings configured for Timeline" if mri.timeline.nil? - end +module ManageIQ + module Reporting + module Formatter + class Timeline < Ruport::Formatter + renders :timeline, :for => ReportRenderer - # Generates the body of the timeline - def build_document_body - mri = options.mri - tz = mri.get_time_zone(Time.zone.name) - # Calculate the earliest time of events to show - unless mri.timeline[:last_unit].nil? - # START of TIMELINE TIMEZONE Code - @start_time = format_timezone(Time.now, tz, 'raw') - mri.timeline[:last_time].to_i.send(mri.timeline[:last_unit].downcase) - # END of TIMELINE TIMEZONE Code - end + # create the graph object and add titles, fonts, and colors + def build_document_header + mri = options.mri + raise "No settings configured for Timeline" if mri.timeline.nil? + end - mri.extras ||= {} # Create hash to store :tl_position setting + # Generates the body of the timeline + def build_document_body + mri = options.mri + tz = mri.get_time_zone(Time.zone.name) + # Calculate the earliest time of events to show + unless mri.timeline[:last_unit].nil? + # START of TIMELINE TIMEZONE Code + @start_time = format_timezone(Time.now, tz, 'raw') - mri.timeline[:last_time].to_i.send(mri.timeline[:last_unit].downcase) + # END of TIMELINE TIMEZONE Code + end - @events = [] - @events_data = [] - tlfield = mri.timeline[:field].split("-") # Split the table and field - if tlfield.first.include?(".") # If table has a period (from a sub table) - col = tlfield.first.split(".").last + "." + tlfield.last # use subtable.field - else - col = tlfield.last # Not a subtable, just grab the field name - end + mri.extras ||= {} # Create hash to store :tl_position setting - # some of the OOTB reports have db as EventStream or PolicyEvent, - # those do not have event categories, so need to go thru else block for such reports. - if (mri.db == "EventStream" || mri.db == "PolicyEvent") && mri.rpt_options.try(:[], :categories) - event_map = mri.table.data.each_with_object({}) do |event, buckets| - bucket_name = mri.rpt_options[:categories].detect do |_, options| - options[:include_set].include?(event.event_type) - end&.last.try(:[], :display_name) + @events = [] + @events_data = [] + tlfield = mri.timeline[:field].split("-") # Split the table and field + if tlfield.first.include?(".") # If table has a period (from a sub table) + col = tlfield.first.split(".").last + "." + tlfield.last # use subtable.field + else + col = tlfield.last # Not a subtable, just grab the field name + end - bucket_name ||= mri.rpt_options[:categories].detect do |_, options| - options[:regexes].any? { |regex| regex.match(event.event_type) } - end.last[:display_name] + # some of the OOTB reports have db as EventStream or PolicyEvent, + # those do not have event categories, so need to go thru else block for such reports. + if (mri.db == "EventStream" || mri.db == "PolicyEvent") && mri.rpt_options.try(:[], :categories) + event_map = mri.table.data.each_with_object({}) do |event, buckets| + bucket_name = mri.rpt_options[:categories].detect do |_, options| + options[:include_set].include?(event.event_type) + end&.last.try(:[], :display_name) - buckets[bucket_name] ||= [] - buckets[bucket_name] << event - end + bucket_name ||= mri.rpt_options[:categories].detect do |_, options| + options[:regexes].any? { |regex| regex.match(event.event_type) } + end.last[:display_name] - event_map.each do |name, events| - @events_data = [] - events.each { |row| tl_event(row, col) } - @events.push(:name => name, :data => [@events_data]) - end - else - mri.table.data.each_with_index do |row, _d_idx| - tl_event(row, col) # Add this row to the tl event xml - end - @events.push(:data => [@events_data]) - end - # START of TIMELINE TIMEZONE Code - mri.extras[:tl_position] ||= format_timezone(Time.now, tz, 'raw') # If position not set, default to now - # END of TIMELINE TIMEZONE Code - output << @events.to_json - end + buckets[bucket_name] ||= [] + buckets[bucket_name] << event + end - def tl_event(row, col) - mri = options.mri - tz = mri.get_time_zone(Time.zone.name) - etime = row[col] - return if etime.nil? # Skip nil dates - Sprint 41 - return if !@start_time.nil? && etime < @start_time # Skip if before start time limit - # START of TIMELINE TIMEZONE Code - mri.extras[:tl_position] ||= format_timezone(etime.to_time, tz, 'raw') - if mri.timeline[:position] && mri.timeline[:position] == "First" - mri.extras[:tl_position] = format_timezone(etime.to_time, tz, 'raw') if format_timezone(etime.to_time, tz, 'raw') < format_timezone(mri.extras[:tl_position], tz, 'raw') - elsif mri.timeline[:position] && mri.timeline[:position] == "Current" - # if there is item with current time or greater then use that else, use right most one. - if format_timezone(etime.to_time, tz, 'raw') >= format_timezone(Time.now, tz, 'raw') && format_timezone(etime.to_time, tz, 'raw') <= format_timezone(mri.extras[:tl_position], tz, 'raw') - mri.extras[:tl_position] = format_timezone(etime.to_time, tz, 'raw') - else - mri.extras[:tl_position] = format_timezone(etime.to_time, tz, 'raw') if format_timezone(etime.to_time, tz, 'raw') > format_timezone(mri.extras[:tl_position], tz, 'raw') + event_map.each do |name, events| + @events_data = [] + events.each { |row| tl_event(row, col) } + @events.push(:name => name, :data => [@events_data]) + end + else + mri.table.data.each_with_index do |row, _d_idx| + tl_event(row, col) # Add this row to the tl event xml + end + @events.push(:data => [@events_data]) + end + # START of TIMELINE TIMEZONE Code + mri.extras[:tl_position] ||= format_timezone(Time.now, tz, 'raw') # If position not set, default to now + # END of TIMELINE TIMEZONE Code + output << @events.to_json end - else - mri.extras[:tl_position] = format_timezone(etime.to_time, tz, 'raw') if format_timezone(etime.to_time, tz, 'raw') > format_timezone(mri.extras[:tl_position], tz, 'raw') - end - # END of TIMELINE TIMEZONE Code - if row["id"] # Make sure id column is present - rec = mri.db.constantize.find_by_id(row['id']) - end - unless rec.nil? - case mri.db - when "Vm" - e_title = rec[:name] - when "Host" - e_title = rec[:name] - when "EventStream" - ems_cloud = false - if rec[:ems_id] && ExtManagementSystem.exists?(rec[:ems_id]) - ems = ExtManagementSystem.find(rec[:ems_id]) - ems_cloud = true if ems.kind_of?(EmsCloud) - ems_container = true if ems.kind_of?(::ManageIQ::Providers::ContainerManager) + + def tl_event(row, col) + mri = options.mri + tz = mri.get_time_zone(Time.zone.name) + etime = row[col] + return if etime.nil? # Skip nil dates - Sprint 41 + return if !@start_time.nil? && etime < @start_time # Skip if before start time limit + # START of TIMELINE TIMEZONE Code + mri.extras[:tl_position] ||= format_timezone(etime.to_time, tz, 'raw') + if mri.timeline[:position] && mri.timeline[:position] == "First" + mri.extras[:tl_position] = format_timezone(etime.to_time, tz, 'raw') if format_timezone(etime.to_time, tz, 'raw') < format_timezone(mri.extras[:tl_position], tz, 'raw') + elsif mri.timeline[:position] && mri.timeline[:position] == "Current" + # if there is item with current time or greater then use that else, use right most one. + if format_timezone(etime.to_time, tz, 'raw') >= format_timezone(Time.now, tz, 'raw') && format_timezone(etime.to_time, tz, 'raw') <= format_timezone(mri.extras[:tl_position], tz, 'raw') + mri.extras[:tl_position] = format_timezone(etime.to_time, tz, 'raw') + else + mri.extras[:tl_position] = format_timezone(etime.to_time, tz, 'raw') if format_timezone(etime.to_time, tz, 'raw') > format_timezone(mri.extras[:tl_position], tz, 'raw') + end + else + mri.extras[:tl_position] = format_timezone(etime.to_time, tz, 'raw') if format_timezone(etime.to_time, tz, 'raw') > format_timezone(mri.extras[:tl_position], tz, 'raw') end - if !ems_cloud - e_title = if rec[:vm_name] # Create the title using VM name - rec[:vm_name] - elsif rec[:host_name] # or Host Name - rec[:host_name] - elsif rec[:ems_cluster_name] # or Cluster Name - rec[:ems_cluster_name] - elsif rec[:container_name] - rec[:container_name] - elsif rec[:container_group_name] - rec[:container_group_name] - elsif rec[:container_replicator_name] - rec[:container_replicator_name] - elsif rec[:container_node_name] - rec[:container_node_name] - end + # END of TIMELINE TIMEZONE Code + if row["id"] # Make sure id column is present + rec = mri.db.constantize.find_by_id(row['id']) end - else - e_title = rec[:name] ? rec[:name] : row[mri.col_order.first].to_s - end - end - e_title ||= ems ? ems.name : "No VM, Host, or MS" + unless rec.nil? + case mri.db + when "Vm" + e_title = rec[:name] + when "Host" + e_title = rec[:name] + when "EventStream" + ems_cloud = false + if rec[:ems_id] && ExtManagementSystem.exists?(rec[:ems_id]) + ems = ExtManagementSystem.find(rec[:ems_id]) + ems_cloud = true if ems.kind_of?(EmsCloud) + ems_container = true if ems.kind_of?(::ManageIQ::Providers::ContainerManager) + end + if !ems_cloud + e_title = if rec[:vm_name] # Create the title using VM name + rec[:vm_name] + elsif rec[:host_name] # or Host Name + rec[:host_name] + elsif rec[:ems_cluster_name] # or Cluster Name + rec[:ems_cluster_name] + elsif rec[:container_name] + rec[:container_name] + elsif rec[:container_group_name] + rec[:container_group_name] + elsif rec[:container_replicator_name] + rec[:container_replicator_name] + elsif rec[:container_node_name] + rec[:container_node_name] + end + end + else + e_title = rec[:name] ? rec[:name] : row[mri.col_order.first].to_s + end + end + e_title ||= ems ? ems.name : "No VM, Host, or MS" - # manipulating column order to display timestamp at the end of the bubble. - field = mri.timeline[:field].split("-") - if ems && ems_cloud - # Change labels to be cloud specific - vm_name_idx = mri.col_order.index("vm_name") - mri.headers[vm_name_idx] = "Source Instance" if vm_name_idx - vm_location_idx = mri.col_order.index("vm_location") - mri.headers[vm_location_idx] = "Source Instance Location" if vm_location_idx - dest_vm_name_idx = mri.col_order.index("dest_vm_name") - mri.headers[dest_vm_name_idx] = "Destination Instance" if dest_vm_name_idx - dest_vm_location_idx = mri.col_order.index("dest_vm_location") - mri.headers[dest_vm_location_idx] = "Destination Instance Location" if dest_vm_location_idx - else - mri.col_order.delete("availability_zone.name") - mri.headers.delete("Availability Zone") - end - col_order = copy_array(mri.col_order) - headers = copy_array(mri.headers) - i = col_order.rindex(field.last) - if i.nil? - # Adding a check incase timeline field came in with model/table in front of them - # i.e. PolicyEvent.miq_policy_sets-created_on - field_with_prefix = "#{field.first.split('.').last}.#{field.last}" - i = col_order.rindex(field_with_prefix) - col_order.delete(field_with_prefix) - col_order.push(field_with_prefix) - else - col_order.delete(field.last) - col_order.push(field.last) - end - j = headers[i] - headers.delete(j) - headers.push(j) + # manipulating column order to display timestamp at the end of the bubble. + field = mri.timeline[:field].split("-") + if ems && ems_cloud + # Change labels to be cloud specific + vm_name_idx = mri.col_order.index("vm_name") + mri.headers[vm_name_idx] = "Source Instance" if vm_name_idx + vm_location_idx = mri.col_order.index("vm_location") + mri.headers[vm_location_idx] = "Source Instance Location" if vm_location_idx + dest_vm_name_idx = mri.col_order.index("dest_vm_name") + mri.headers[dest_vm_name_idx] = "Destination Instance" if dest_vm_name_idx + dest_vm_location_idx = mri.col_order.index("dest_vm_location") + mri.headers[dest_vm_location_idx] = "Destination Instance Location" if dest_vm_location_idx + else + mri.col_order.delete("availability_zone.name") + mri.headers.delete("Availability Zone") + end + col_order = copy_array(mri.col_order) + headers = copy_array(mri.headers) + i = col_order.rindex(field.last) + if i.nil? + # Adding a check incase timeline field came in with model/table in front of them + # i.e. PolicyEvent.miq_policy_sets-created_on + field_with_prefix = "#{field.first.split('.').last}.#{field.last}" + i = col_order.rindex(field_with_prefix) + col_order.delete(field_with_prefix) + col_order.push(field_with_prefix) + else + col_order.delete(field.last) + col_order.push(field.last) + end + j = headers[i] + headers.delete(j) + headers.push(j) - flags = {:ems_cloud => ems_cloud, - :ems_container => ems_container, - :time_zone => tz} - tl_message = TimelineMessage.new(row, rec, flags, mri.db) - event_data = {} - col_order.each_with_index do |co, co_idx| - value = tl_message.message_html(co) - next if value.to_s.empty? || co == "id" - event_data[co] = { - :value => value, - :text => headers[co_idx] - } - end + flags = {:ems_cloud => ems_cloud, + :ems_container => ems_container, + :time_zone => tz} + tl_message = TimelineMessage.new(row, rec, flags, mri.db) + event_data = {} + col_order.each_with_index do |co, co_idx| + value = tl_message.message_html(co) + next if value.to_s.empty? || co == "id" + event_data[co] = { + :value => value, + :text => headers[co_idx] + } + end - # Add the event to the timeline - @events_data.push("start" => format_timezone(row[col], tz, 'view'), - "title" => e_title.length < 20 ? e_title : e_title[0...17] + "...", - "event" => event_data) + # Add the event to the timeline + @events_data.push("start" => format_timezone(row[col], tz, 'view'), + "title" => e_title.length < 20 ? e_title : e_title[0...17] + "...", + "event" => event_data) + end + end end end end diff --git a/lib/manageiq/reporting/formatter/timeline_message.rb b/lib/manageiq/reporting/formatter/timeline_message.rb index 565f644a32a..0979537c200 100644 --- a/lib/manageiq/reporting/formatter/timeline_message.rb +++ b/lib/manageiq/reporting/formatter/timeline_message.rb @@ -1,150 +1,154 @@ -module ReportFormatter - class TimelineMessage - TIMELINE_TIME_COLUMNS = %w(created_on timestamp).freeze - - def initialize(row, event, flags, db) - @row, @event, @flags, @db = row, event, flags, db - end - - def message_html(column) - @column = column - field = column.tr('.', '_').to_sym - respond_to?(field, true) ? send(field).to_s : text - end +module ManageIQ + module Reporting + module Formatter + class TimelineMessage + TIMELINE_TIME_COLUMNS = %w(created_on timestamp).freeze + + def initialize(row, event, flags, db) + @row, @event, @flags, @db = row, event, flags, db + end - private + def message_html(column) + @column = column + field = column.tr('.', '_').to_sym + respond_to?(field, true) ? send(field).to_s : text + end - def vm_name - "#{text}" if @event.vm_or_template_id - end + private - def src_vm_name - "#{text}" if @event.src_vm_or_template - end + def vm_name + "#{text}" if @event.vm_or_template_id + end - def dest_vm_name - "#{text}" if @event.dest_vm_or_template_id - end + def src_vm_name + "#{text}" if @event.src_vm_or_template + end - def host_name - "#{text}" if @event.host_id - end + def dest_vm_name + "#{text}" if @event.dest_vm_or_template_id + end - def dest_host_name - "#{text}" if @event.dest_host_id - end + def host_name + "#{text}" if @event.host_id + end - def target_name - e_text = if @event.target_name # Create the title using Policy description - @event.target_name - elsif @event.miq_policy_id && MiqPolicy.exists?(@event.miq_policy_id) # or Policy name - MiqPolicy.find(@event.miq_policy_id).name - else - _("Policy no longer exists") - end - unless @event.target_id.nil? - e_text += "
#{Dictionary.gettext(@event.target_class, :type => :model, :notfound => :titleize)}: " - e_text += "#{@event.target_name}" - end - assigned_profiles = @event.miq_policy_sets.each_with_object({}) do |profile, hsh| - hsh[profile.id] = profile.description unless profile.description.nil? - end + def dest_host_name + "#{text}" if @event.dest_host_id + end - unless @event.event_type.nil? - e_text += "
#{_("Assigned Profiles")}: " - assigned_profiles.each_with_index do |p, i| - e_text += "#{p[1]}" - e_text += ", " if assigned_profiles.length > 1 && i < assigned_profiles.length + def target_name + e_text = if @event.target_name # Create the title using Policy description + @event.target_name + elsif @event.miq_policy_id && MiqPolicy.exists?(@event.miq_policy_id) # or Policy name + MiqPolicy.find(@event.miq_policy_id).name + else + _("Policy no longer exists") + end + unless @event.target_id.nil? + e_text += "
#{Dictionary.gettext(@event.target_class, :type => :model, :notfound => :titleize)}: " + e_text += "#{@event.target_name}" + end + assigned_profiles = @event.miq_policy_sets.each_with_object({}) do |profile, hsh| + hsh[profile.id] = profile.description unless profile.description.nil? + end + + unless @event.event_type.nil? + e_text += "
#{_("Assigned Profiles")}: " + assigned_profiles.each_with_index do |p, i| + e_text += "#{p[1]}" + e_text += ", " if assigned_profiles.length > 1 && i < assigned_profiles.length + end + end + e_text end - end - e_text - end - def ems_cluster_name - "#{text}" if @event.ems_cluster_id - end + def ems_cluster_name + "#{text}" if @event.ems_cluster_id + end - def availability_zone_name - if @event.availability_zone_id - "#{text}" - end - end + def availability_zone_name + if @event.availability_zone_id + "#{text}" + end + end - def container_node_name - "#{text}" if @event.container_node_id - end + def container_node_name + "#{text}" if @event.container_node_id + end - def container_group_name - "#{text}" if @event.container_group_id - end + def container_group_name + "#{text}" if @event.container_group_id + end - def container_name - "#{text}" if @event.container_id - end + def container_name + "#{text}" if @event.container_id + end - def container_replicator_name - if @event.container_replicator_id - "#{text}" - end - end + def container_replicator_name + if @event.container_replicator_id + "#{text}" + end + end - def middleware_name - mw_id_cols = EmsEvent.column_names.select { |n| n.match('middleware_.+_id') } - mw_id_col = mw_id_cols.find { |c| @event[c] } - unless mw_id_col.nil? - mw_type = mw_id_col.slice(0, mw_id_col.rindex('_id')) - mw_name_col = mw_type + '_name' - "#{@event[mw_name_col]}" - end - end + def middleware_name + mw_id_cols = EmsEvent.column_names.select { |n| n.match('middleware_.+_id') } + mw_id_col = mw_id_cols.find { |c| @event[c] } + unless mw_id_col.nil? + mw_type = mw_id_col.slice(0, mw_id_col.rindex('_id')) + mw_name_col = mw_type + '_name' + "#{@event[mw_name_col]}" + end + end - def ext_management_system_name - if @event.ext_management_system && @event.ext_management_system.id - provider_id = @event.ext_management_system.id - if ems_cloud - # restful route is used for cloud provider unlike infrastructure provider - "#{text}" - elsif ems_container - "#{text}" - elsif ems_mw - "#{text}" - else - "#{text}" + def ext_management_system_name + if @event.ext_management_system && @event.ext_management_system.id + provider_id = @event.ext_management_system.id + if ems_cloud + # restful route is used for cloud provider unlike infrastructure provider + "#{text}" + elsif ems_container + "#{text}" + elsif ems_mw + "#{text}" + else + "#{text}" + end + end end - end - end - def resource_name - if @db == 'BottleneckEvent' - db = if ems_cloud && @event.resource_type == 'ExtManagementSystem' - 'ems_cloud' - elsif @event.resource_type == 'ExtManagementSystem' - 'ems_infra' - else - "#{@event.resource_type.underscore}/show" - end - "#{@event.resource_name}" - end - end + def resource_name + if @db == 'BottleneckEvent' + db = if ems_cloud && @event.resource_type == 'ExtManagementSystem' + 'ems_cloud' + elsif @event.resource_type == 'ExtManagementSystem' + 'ems_infra' + else + "#{@event.resource_type.underscore}/show" + end + "#{@event.resource_name}" + end + end - def text - if @row[@column].kind_of?(Time) || TIMELINE_TIME_COLUMNS.include?(@column) - format_timezone(Time.parse(@row[@column].to_s).utc, @flags[:time_zone], "gtl") - else - @row[@column].to_s - end - end + def text + if @row[@column].kind_of?(Time) || TIMELINE_TIME_COLUMNS.include?(@column) + format_timezone(Time.parse(@row[@column].to_s).utc, @flags[:time_zone], "gtl") + else + @row[@column].to_s + end + end - def ems_cloud - @flags[:ems_cloud] - end + def ems_cloud + @flags[:ems_cloud] + end - def ems_container - @flags[:ems_container] - end + def ems_container + @flags[:ems_container] + end - def ems_mw - @flags[:ems_mw] + def ems_mw + @flags[:ems_mw] + end + end end end end diff --git a/lib/report_formatter.rb b/lib/report_formatter.rb new file mode 100644 index 00000000000..4ab3b28ac10 --- /dev/null +++ b/lib/report_formatter.rb @@ -0,0 +1,6 @@ +# If code uses the old constant name: +# * Rails will autoload it and start here. +# * We assign the old toplevel constant to the new constant. +# * We can't include rails deprecate_constant globally, so we use ruby's. +ReportFormatter = ManageIQ::Reporting::Formatter +Object.deprecate_constant :ReportFormatter diff --git a/spec/lib/manageiq/reporting/formatter/c3_spec.rb b/spec/lib/manageiq/reporting/formatter/c3_spec.rb index 09e85f1b879..4a634e265ae 100644 --- a/spec/lib/manageiq/reporting/formatter/c3_spec.rb +++ b/spec/lib/manageiq/reporting/formatter/c3_spec.rb @@ -1,9 +1,9 @@ -describe ReportFormatter::C3Formatter do +describe ManageIQ::Reporting::Formatter::C3 do include Spec::Support::ReportHelper before do - allow(Charting).to receive(:backend).and_return(:c3) - allow(Charting).to receive(:format).and_return(:c3) + allow(ManageIQ::Reporting::Charting).to receive(:backend).and_return(:c3) + allow(ManageIQ::Reporting::Charting).to receive(:format).and_return(:c3) end describe "#add_series" do diff --git a/spec/lib/manageiq/reporting/formatter/chart_common_spec.rb b/spec/lib/manageiq/reporting/formatter/chart_common_spec.rb index 584fbe8d569..cd56330f03c 100644 --- a/spec/lib/manageiq/reporting/formatter/chart_common_spec.rb +++ b/spec/lib/manageiq/reporting/formatter/chart_common_spec.rb @@ -8,12 +8,12 @@ # # ./lib/report_formatter/chart_common.rb:555:in `build_performance_chart' # # ./lib/report_formatter/chart_common.rb:57:in `call' -describe ReportFormatter::C3Formatter do +describe ManageIQ::Reporting::Formatter::C3 do include Spec::Support::ReportHelper before do - allow(Charting).to receive(:backend).and_return(:c3) - allow(Charting).to receive(:format).and_return(:c3) + allow(ManageIQ::Reporting::Charting).to receive(:backend).and_return(:c3) + allow(ManageIQ::Reporting::Charting).to receive(:format).and_return(:c3) end context '#build_performance_chart_area' do it "builds a daily chart with all nils" do diff --git a/spec/lib/manageiq/reporting/formatter/text_spec.rb b/spec/lib/manageiq/reporting/formatter/text_spec.rb index 33f27c04921..334bf5372b8 100644 --- a/spec/lib/manageiq/reporting/formatter/text_spec.rb +++ b/spec/lib/manageiq/reporting/formatter/text_spec.rb @@ -1,9 +1,9 @@ -describe ReportFormatter::ReportText do +describe ManageIQ::Reporting::Formatter::Text do include Spec::Support::ReportHelper before do - allow(Charting).to receive(:backend).and_return(:text) - allow(Charting).to receive(:format).and_return(:text) + allow(ManageIQ::Reporting::Charting).to receive(:backend).and_return(:text) + allow(ManageIQ::Reporting::Charting).to receive(:format).and_return(:text) end it "expands report width for really long filter condition" do diff --git a/spec/lib/manageiq/reporting/formatter/timeline_message_spec.rb b/spec/lib/manageiq/reporting/formatter/timeline_message_spec.rb index c84460caad1..785c1805c23 100644 --- a/spec/lib/manageiq/reporting/formatter/timeline_message_spec.rb +++ b/spec/lib/manageiq/reporting/formatter/timeline_message_spec.rb @@ -1,4 +1,4 @@ -describe ReportFormatter::TimelineMessage do +describe ManageIQ::Reporting::Formatter::TimelineMessage do context "#message_html" do context "for unknown column names" do subject { described_class.new({'column' => @value}, nil, {:time_zone => 'Mexico City'}, nil).message_html('column') } diff --git a/spec/lib/manageiq/reporting/formatter/timeline_spec.rb b/spec/lib/manageiq/reporting/formatter/timeline_spec.rb index b1ac3f2cea7..68534917662 100644 --- a/spec/lib/manageiq/reporting/formatter/timeline_spec.rb +++ b/spec/lib/manageiq/reporting/formatter/timeline_spec.rb @@ -1,4 +1,4 @@ -describe ReportFormatter::TimelineMessage do +describe ManageIQ::Reporting::Formatter::TimelineMessage do describe '#message_html on container event' do row = {} let(:ems) { FactoryBot.create(:ems_redhat, :id => 42) } @@ -22,7 +22,7 @@ tests.each do |column, href| it "Evaluate column #{column} content" do row[column] = 'test timeline' - val = ReportFormatter::TimelineMessage.new(row, event, flags, 'EmsEvent').message_html(column) + val = ManageIQ::Reporting::Formatter::TimelineMessage.new(row, event, flags, 'EmsEvent').message_html(column) expect(val).to eq(href) end end @@ -47,7 +47,7 @@ tests.each do |column, href| it "Evaluate column #{column} content" do row[column] = 'test timeline' - val = ReportFormatter::TimelineMessage.new(row, event, flags, 'EmsEvent').message_html(column) + val = ManageIQ::Reporting::Formatter::TimelineMessage.new(row, event, flags, 'EmsEvent').message_html(column) expect(val).to eq(href) end end @@ -73,7 +73,7 @@ before { event.contents << event_content } - subject { ReportFormatter::TimelineMessage.new({'event_type' => 'vm_poweroff'}, event, {}, 'PolicyEvent').message_html('target_name') } + subject { ManageIQ::Reporting::Formatter::TimelineMessage.new({'event_type' => 'vm_poweroff'}, event, {}, 'PolicyEvent').message_html('target_name') } it 'generates a link to the affected policy profile' do is_expected.to include("?profile=#{policy_set.id}") @@ -83,7 +83,7 @@ tests.each do |column, href| it "Evaluate column #{column} content" do row[column] = 'vm_poweroff' - val = ReportFormatter::TimelineMessage.new(row, event, {}, 'PolicyEvent').message_html(column) + val = ManageIQ::Reporting::Formatter::TimelineMessage.new(row, event, {}, 'PolicyEvent').message_html(column) expect(val).to eq(href) end end @@ -131,7 +131,7 @@ def stub_ems_event(event_type) end it 'shows correct count of timeline events based on categories' do - formatter = ReportFormatter::ReportTimeline.new + formatter = ManageIQ::Reporting::Formatter::Timeline.new formatter.options.mri = @report events = formatter.build_document_body expect(JSON.parse(events)[0]["data"][0].length).to eq(30) @@ -140,7 +140,7 @@ def stub_ems_event(event_type) it 'shows correct count of timeline events together for report object with no categories' do @report.rpt_options = {} - formatter = ReportFormatter::ReportTimeline.new + formatter = ManageIQ::Reporting::Formatter::Timeline.new formatter.options.mri = @report events = formatter.build_document_body expect(JSON.parse(events)[0]["data"][0].length).to eq(45) @@ -148,7 +148,7 @@ def stub_ems_event(event_type) it 'shows correct count of timeline events for timeline based report when rpt_options is nil' do @report.rpt_options = nil - formatter = ReportFormatter::ReportTimeline.new + formatter = ManageIQ::Reporting::Formatter::Timeline.new formatter.options.mri = @report events = formatter.build_document_body expect(JSON.parse(events)[0]["data"][0].length).to eq(45) @@ -216,7 +216,7 @@ def stub_ems_event(event_type) end it 'shows correct count of timeline events based on categories' do - formatter = ReportFormatter::ReportTimeline.new + formatter = ManageIQ::Reporting::Formatter::Timeline.new formatter.options.mri = @report events = formatter.build_document_body expect(JSON.parse(events)[0]["data"][0].length).to eq(5) @@ -225,7 +225,7 @@ def stub_ems_event(event_type) it 'shows correct count of timeline events together for report object with no categories' do @report.rpt_options = {} - formatter = ReportFormatter::ReportTimeline.new + formatter = ManageIQ::Reporting::Formatter::Timeline.new formatter.options.mri = @report events = formatter.build_document_body expect(JSON.parse(events)[0]["data"][0].length).to eq(12) @@ -233,7 +233,7 @@ def stub_ems_event(event_type) it 'shows correct count of timeline events for timeline based report when rpt_options is nil' do @report.rpt_options = nil - formatter = ReportFormatter::ReportTimeline.new + formatter = ManageIQ::Reporting::Formatter::Timeline.new formatter.options.mri = @report events = formatter.build_document_body expect(JSON.parse(events)[0]["data"][0].length).to eq(12) @@ -275,7 +275,7 @@ def stub_ems_event(event_type) it 'shows headers only if they exist in report col headers' do @report.rpt_options = nil - formatter = ReportFormatter::ReportTimeline.new + formatter = ManageIQ::Reporting::Formatter::Timeline.new formatter.options.mri = @report events = formatter.build_document_body json = JSON.parse(events)[0]["data"][0][0]["event"] diff --git a/spec/models/miq_report/charting_spec.rb b/spec/models/miq_report/charting_spec.rb index fa6cb9b84df..f648ecd4422 100644 --- a/spec/models/miq_report/charting_spec.rb +++ b/spec/models/miq_report/charting_spec.rb @@ -16,8 +16,8 @@ @show_title = true @options = MiqReport.graph_options({ :title => "CPU (Mhz)", :type => "Line", :columns => ["col"] }) - allow(Charting).to receive(:backend).and_return(:c3) - allow(Charting).to receive(:format).and_return(:c3) + allow(ManageIQ::Reporting::Charting).to receive(:backend).and_return(:c3) + allow(ManageIQ::Reporting::Charting).to receive(:format).and_return(:c3) end context 'graph_options' do diff --git a/spec/models/miq_report_result_spec.rb b/spec/models/miq_report_result_spec.rb index 2ae0814012e..2e24fa60a11 100644 --- a/spec/models/miq_report_result_spec.rb +++ b/spec/models/miq_report_result_spec.rb @@ -105,7 +105,7 @@ @show_title = true @options = MiqReport.graph_options({ :title => "CPU (Mhz)", :type => "Line", :columns => ["col"] }) - allow(Charting).to receive(:detect_available_plugin).and_return(C3Charting) + allow(ManageIQ::Reporting::Charting).to receive(:detect_available_plugin).and_return(ManageIQ::Reporting::C3Charting) end it "should save the original report metadata and the generated table as a binary blob" do diff --git a/spec/models/miq_widget/chart_content_spec.rb b/spec/models/miq_widget/chart_content_spec.rb index 218d4975ce6..50e5149ffd3 100644 --- a/spec/models/miq_widget/chart_content_spec.rb +++ b/spec/models/miq_widget/chart_content_spec.rb @@ -60,6 +60,6 @@ it '#generate returns valid data' do content = widget.generate_one_content_for_user(@group, @user) - expect(Charting.data_ok? content.contents).to eq(true) + expect(ManageIQ::Reporting::Charting.data_ok? content.contents).to eq(true) end end diff --git a/spec/support/report_helper.rb b/spec/support/report_helper.rb index 2a563ddb2fc..cf9c03b262d 100644 --- a/spec/support/report_helper.rb +++ b/spec/support/report_helper.rb @@ -2,7 +2,7 @@ module Spec module Support module ReportHelper def render_report(report) - ReportFormatter::ReportRenderer.render(Charting.format) do |e| + ManageIQ::Reporting::Formatter::ReportRenderer.render(ManageIQ::Reporting::Charting.format) do |e| e.options.mri = report e.options.show_title = true e.options.graph_options = MiqReport.graph_options From bf0bfd4574b66a9edf5f92f0c478cb1f3eba59ce Mon Sep 17 00:00:00 2001 From: Joe Rafaniello Date: Wed, 26 Feb 2020 09:23:59 -0500 Subject: [PATCH 4/4] Move existing codeowner from UI-classic to core, add me From #19863 Related to: https://github.com/ManageIQ/manageiq/issues/19674 (missing ReportFormatter for backend workers) https://github.com/ManageIQ/manageiq/issues/19863 (move code from UI-classic that can be called from either the frontend or backend) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9b1d6c5c06f..6872bb6d7d9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -57,4 +57,5 @@ /content @bdunne /config @jrafanie /db @agrare @carboni @bdunne +/lib/manageiq/reporting @panspagetka @jrafanie /product* @hkataria