-
Notifications
You must be signed in to change notification settings - Fork 900
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reviewers, let me explain this. Rails has Unfortunately, you can't include that module globally, and we have two global constants, So, here, we setup the old Constant, set to the new one and deprecate the old one. |
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 } |
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 |
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 |
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 |
There was a problem hiding this comment.
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.