Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move report formatter and charting to core #19873

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@
/content @bdunne
/config @jrafanie
/db @agrare @carboni @bdunne
/lib/manageiq/reporting @panspagetka @jrafanie
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add @PanSpagetka and I as the reviewers for this directory from a code and code loading side.

/product* @hkataria
2 changes: 1 addition & 1 deletion app/models/miq_report/formatters/graph.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
2 changes: 1 addition & 1 deletion app/models/miq_widget/chart_content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions lib/charting.rb
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member Author

@jrafanie jrafanie Feb 26, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewers, let me explain this. Rails has deprecate_constant that you can use by including ActiveSupport::Deprecation::DeprecatedConstantAccessor in your class/module. This is great and works well in the old ReportFormatter, see the lib/manageiq/reporting/formatter.rb. The major benefit is it tells you what is deprecated and what's the new behavior to use.

Unfortunately, you can't include that module globally, and we have two global constants, Charting and ReportFormatter to deprecate. Ruby has deprecate_constant, which we can call on Object to ensure any references to Charting ReportFormatter at the top level do raise deprecations. The ruby version doesn't need to include any modules but it also just tells you it's deprecated without any suggestions as to the solution. I think this is the best we can do. Maybe someone knows something better?

So, here, we setup the old Constant, set to the new one and deprecate the old one.

40 changes: 40 additions & 0 deletions lib/manageiq/reporting/charting.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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

def self.new
self == ManageIQ::Reporting::Charting ? detect_available_plugin.new : super
end

def self.detect_available_plugin
subclasses.select(&:available?).max_by(&:priority)
end
end
end
end

# load all plugins
Dir.glob(File.join(File.dirname(__FILE__), "charting/*.rb")).each { |f| require_dependency f }
96 changes: 96 additions & 0 deletions lib/manageiq/reporting/charting/c3_charting.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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

# 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 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 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
end
end
39 changes: 39 additions & 0 deletions lib/manageiq/reporting/formatter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
include ActionView::Helpers::NumberHelper

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
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
189 changes: 189 additions & 0 deletions lib/manageiq/reporting/formatter/c3.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
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

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(/(?<!:):(?!:)/)[0], custom_format)

if format
axis_formatter = {:function => 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
end
end
Loading