reports_kit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -5
  3. data/app/assets/javascripts/reports_kit/lib/chart.js +33 -8
  4. data/app/assets/javascripts/reports_kit/lib/report.js +27 -26
  5. data/app/assets/javascripts/reports_kit/lib/table.js +58 -0
  6. data/app/assets/javascripts/reports_kit/vendor/jquery.tablesorter.min.js +4 -0
  7. data/app/assets/stylesheets/reports_kit/reports.css.sass +20 -0
  8. data/config/initializers/mime_types.rb +1 -0
  9. data/docs/dimensions.md +34 -26
  10. data/docs/display_options.md +15 -12
  11. data/docs/filters.md +2 -2
  12. data/docs/measures.md +4 -3
  13. data/gemfiles/mysql.gemfile.lock +14 -1
  14. data/gemfiles/postgresql.gemfile.lock +14 -1
  15. data/lib/reports_kit.rb +15 -0
  16. data/lib/reports_kit/cache.rb +37 -0
  17. data/lib/reports_kit/configuration.rb +13 -1
  18. data/lib/reports_kit/helper.rb +54 -3
  19. data/lib/reports_kit/model_configuration.rb +6 -1
  20. data/lib/reports_kit/order.rb +33 -0
  21. data/lib/reports_kit/relative_time.rb +42 -0
  22. data/lib/reports_kit/report_builder.rb +34 -15
  23. data/lib/reports_kit/reports/abstract_measure.rb +9 -0
  24. data/lib/reports_kit/reports/adapters/mysql.rb +8 -1
  25. data/lib/reports_kit/reports/adapters/postgresql.rb +8 -1
  26. data/lib/reports_kit/reports/composite_measure.rb +43 -0
  27. data/lib/reports_kit/reports/data/chart_options.rb +5 -0
  28. data/lib/reports_kit/reports/data/composite_aggregation.rb +96 -0
  29. data/lib/reports_kit/reports/data/entity.rb +7 -0
  30. data/lib/reports_kit/reports/data/format_one_dimension.rb +120 -0
  31. data/lib/reports_kit/reports/data/format_two_dimensions.rb +141 -0
  32. data/lib/reports_kit/reports/data/generate.rb +72 -25
  33. data/lib/reports_kit/reports/data/generate_for_properties.rb +75 -0
  34. data/lib/reports_kit/reports/data/one_dimension.rb +15 -49
  35. data/lib/reports_kit/reports/data/populate_one_dimension.rb +36 -0
  36. data/lib/reports_kit/reports/data/populate_two_dimensions.rb +104 -0
  37. data/lib/reports_kit/reports/data/two_dimensions.rb +15 -110
  38. data/lib/reports_kit/reports/data/utils.rb +77 -12
  39. data/lib/reports_kit/reports/data/value.rb +7 -0
  40. data/lib/reports_kit/reports/dimension.rb +4 -110
  41. data/lib/reports_kit/reports/dimension_with_measure.rb +137 -0
  42. data/lib/reports_kit/reports/filter.rb +5 -64
  43. data/lib/reports_kit/reports/filter_types/base.rb +1 -1
  44. data/lib/reports_kit/reports/filter_types/boolean.rb +9 -7
  45. data/lib/reports_kit/reports/filter_types/datetime.rb +7 -5
  46. data/lib/reports_kit/reports/filter_types/number.rb +2 -0
  47. data/lib/reports_kit/reports/filter_with_measure.rb +84 -0
  48. data/lib/reports_kit/reports/generate_autocomplete_results.rb +1 -1
  49. data/lib/reports_kit/reports/inferrable_configuration.rb +32 -13
  50. data/lib/reports_kit/reports/measure.rb +48 -12
  51. data/lib/reports_kit/reports_controller.rb +42 -3
  52. data/lib/reports_kit/version.rb +1 -1
  53. data/reports_kit.gemspec +2 -0
  54. data/spec/fixtures/generate_inputs.yml +146 -21
  55. data/spec/fixtures/generate_outputs.yml +768 -17
  56. data/spec/reports_kit/relative_time_spec.rb +29 -0
  57. data/spec/reports_kit/report_builder_spec.rb +28 -0
  58. data/spec/reports_kit/reports/data/generate_spec.rb +614 -27
  59. data/spec/reports_kit/reports/dimension_with_measure_spec.rb +69 -0
  60. data/spec/reports_kit/reports/{filter_spec.rb → filter_with_measure_spec.rb} +4 -3
  61. data/spec/spec_helper.rb +7 -2
  62. data/spec/support/config.rb +11 -0
  63. data/spec/support/helpers.rb +3 -3
  64. data/spec/support/models/issue.rb +7 -0
  65. metadata +53 -4
  66. data/spec/reports_kit/reports/dimension_spec.rb +0 -54
@@ -27,7 +27,7 @@ module ReportsKit
27
27
  end
28
28
 
29
29
  def criteria
30
- properties[:criteria]
30
+ @criteria ||= default_criteria.merge(properties[:criteria])
31
31
  end
32
32
 
33
33
  def value
@@ -3,7 +3,7 @@ module ReportsKit
3
3
  module FilterTypes
4
4
  class Boolean < Base
5
5
  DEFAULT_CRITERIA = {
6
- operator: nil
6
+ value: nil
7
7
  }
8
8
 
9
9
  def apply_conditions(records)
@@ -11,29 +11,31 @@ module ReportsKit
11
11
  when ::String
12
12
  records.where("(#{conditions}) #{sql_operator} true")
13
13
  when ::Hash
14
- boolean_operator ? records.where(conditions) : records.not.where(conditions)
14
+ boolean_value ? records.where(conditions) : records.not.where(conditions)
15
+ when ::Proc
16
+ conditions.call(records)
15
17
  else
16
18
  raise ArgumentError.new("Unsupported conditions type: '#{conditions}'")
17
19
  end
18
20
  end
19
21
 
20
- def boolean_operator
21
- case criteria[:operator]
22
+ def boolean_value
23
+ case criteria[:value]
22
24
  when true, 'true'
23
25
  true
24
26
  when false, 'false'
25
27
  false
26
28
  else
27
- raise ArgumentError.new("Unsupported operator: '#{criteria[:operator]}'")
29
+ raise ArgumentError.new("Unsupported value: '#{criteria[:value]}'")
28
30
  end
29
31
  end
30
32
 
31
33
  def sql_operator
32
- boolean_operator ? '=' : '!='
34
+ boolean_value ? '=' : '!='
33
35
  end
34
36
 
35
37
  def valid?
36
- criteria[:operator].present?
38
+ criteria[:value].present?
37
39
  end
38
40
 
39
41
  def conditions
@@ -5,6 +5,7 @@ module ReportsKit
5
5
  DEFAULT_CRITERIA = {
6
6
  operator: 'between'
7
7
  }
8
+ SEPARATOR = ' - '
8
9
 
9
10
  def apply_conditions(records)
10
11
  case criteria[:operator]
@@ -17,19 +18,20 @@ module ReportsKit
17
18
 
18
19
  def start_at_end_at
19
20
  @start_at_end_at ||= begin
20
- start_string, end_string = value.split(' - ')
21
- start_at = Date.parse(start_string)
22
- end_at = Date.parse(end_string)
21
+ return unless valid?
22
+ start_string, end_string = value.split(SEPARATOR)
23
+ start_at = ReportsKit::Reports::Data::Utils.parse_date_string(start_string)
24
+ end_at = ReportsKit::Reports::Data::Utils.parse_date_string(end_string).end_of_day
23
25
  [start_at, end_at]
24
26
  end
25
27
  end
26
28
 
27
29
  def start_at
28
- start_at_end_at[0]
30
+ start_at_end_at.try(:[], 0)
29
31
  end
30
32
 
31
33
  def end_at
32
- start_at_end_at[1]
34
+ start_at_end_at.try(:[], 1)
33
35
  end
34
36
 
35
37
  def valid?
@@ -2,6 +2,8 @@ module ReportsKit
2
2
  module Reports
3
3
  module FilterTypes
4
4
  class Number < Base
5
+ DEFAULT_CRITERIA = {}
6
+
5
7
  def apply_conditions(records)
6
8
  case criteria[:operator]
7
9
  when '>'
@@ -0,0 +1,84 @@
1
+ module ReportsKit
2
+ module Reports
3
+ class FilterWithMeasure
4
+ CONFIGURATION_STRATEGIES_FILTER_TYPE_CLASSES = {
5
+ association: FilterTypes::Records,
6
+ boolean: FilterTypes::Boolean,
7
+ datetime: FilterTypes::Datetime,
8
+ integer: FilterTypes::Number,
9
+ string: FilterTypes::String
10
+ }
11
+ COLUMN_TYPES_FILTER_TYPE_CLASSES = {
12
+ boolean: FilterTypes::Boolean,
13
+ datetime: FilterTypes::Datetime,
14
+ integer: FilterTypes::Number,
15
+ string: FilterTypes::String
16
+ }
17
+
18
+ attr_accessor :filter, :measure, :configuration
19
+
20
+ delegate :key, :properties, :label, to: :filter
21
+ delegate :configured_by_association?, :configured_by_column?, :configured_by_model?, :configured_by_time?,
22
+ :settings_from_model, :configuration_strategy, :instance_class, :column_type, :column,
23
+ to: :configuration
24
+
25
+ def initialize(filter:, measure:)
26
+ self.filter = filter
27
+ self.measure = measure
28
+ self.configuration = InferrableConfiguration.new(self, :filters)
29
+
30
+ self.properties[:criteria] = filter_type.default_criteria unless self.properties[:criteria]
31
+ end
32
+
33
+ def normalized_properties
34
+ return properties unless configured_by_time?
35
+ criteria = properties[:criteria]
36
+ return properties if criteria.blank? || criteria[:value].blank?
37
+ values = criteria[:value].split(ReportsKit::Reports::FilterTypes::Datetime::SEPARATOR)
38
+ values = values.map { |value| ReportsKit::Reports::Data::Utils.format_time_value(value) }
39
+ properties[:criteria][:value] = values.join(ReportsKit::Reports::FilterTypes::Datetime::SEPARATOR)
40
+ properties
41
+ end
42
+
43
+ def settings
44
+ inferred_settings.merge(settings_from_model)
45
+ end
46
+
47
+ def inferred_settings
48
+ configuration.inferred_settings.merge(inferred_filter_settings)
49
+ end
50
+
51
+ def inferred_filter_settings
52
+ {
53
+ column: column
54
+ }
55
+ end
56
+
57
+ def type_klass
58
+ type_klass_for_configuration_strategy = CONFIGURATION_STRATEGIES_FILTER_TYPE_CLASSES[configuration_strategy]
59
+ return type_klass_for_configuration_strategy if type_klass_for_configuration_strategy
60
+ type_klass_for_column_type = COLUMN_TYPES_FILTER_TYPE_CLASSES[column_type]
61
+ return type_klass_for_column_type if type_klass_for_column_type
62
+ return filter_type_class_from_model if configured_by_model?
63
+ raise ArgumentError.new("No configuration found for filter with key: '#{key}'")
64
+ end
65
+
66
+ def filter_type
67
+ type_klass.new(settings, properties)
68
+ end
69
+
70
+ def filter_type_class_from_model
71
+ return unless settings
72
+ type_key = settings[:type_key]
73
+ raise ArgumentError.new("No type specified for filter with key: '#{key}'") unless type_key
74
+ type_class = CONFIGURATION_STRATEGIES_FILTER_TYPE_CLASSES[type_key]
75
+ raise ArgumentError.new("Invalid type ('#{type_key}') specified for filter with key: '#{key}'") unless type_class
76
+ type_class
77
+ end
78
+
79
+ def apply(relation)
80
+ filter_type.apply_filter(relation)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -42,7 +42,7 @@ module ReportsKit
42
42
  def model
43
43
  @model ||= begin
44
44
  measure = Measure.new(measure_key, context_record: context_record)
45
- filter = Filter.new(filter_key, measure: measure)
45
+ filter = FilterWithMeasure.new(filter: Filter.new(filter_key), measure: measure)
46
46
  filter.instance_class
47
47
  end
48
48
  end
@@ -73,25 +73,44 @@ module ReportsKit
73
73
  def inferred_settings
74
74
  return { column: "#{model_class.table_name}.#{key}" } if configured_by_column?
75
75
  if configured_by_association?
76
- return { column: "#{model_class.table_name}.#{reflection.foreign_key}" } if reflection.macro == :belongs_to
76
+ return inferred_settings_from_belongs_to_or_has_one if inferred_settings_from_belongs_to_or_has_one
77
77
  return inferred_settings_from_has_many if inferred_settings_from_has_many
78
78
  end
79
79
  {}
80
80
  end
81
81
 
82
+ def inferred_settings_from_belongs_to_or_has_one
83
+ @inferred_settings_from_belongs_to_or_has_one ||= begin
84
+ return unless reflection.macro.in?([:belongs_to, :has_one])
85
+ through_reflection = reflection.through_reflection
86
+ if through_reflection
87
+ {
88
+ joins: through_reflection.name,
89
+ column: "#{through_reflection.table_name}.#{reflection.source_reflection.foreign_key}"
90
+ }
91
+ else
92
+ {
93
+ column: "#{model_class.table_name}.#{reflection.foreign_key}"
94
+ }
95
+ end
96
+ end
97
+ end
98
+
82
99
  def inferred_settings_from_has_many
83
- return unless reflection.macro == :has_many
84
- through_reflection = reflection.through_reflection
85
- if through_reflection
86
- {
87
- joins: through_reflection.name,
88
- column: "#{through_reflection.table_name}.#{reflection.source_reflection.foreign_key}"
89
- }
90
- else
91
- {
92
- joins: reflection.name,
93
- column: "#{reflection.klass.table_name}.#{reflection.klass.primary_key}"
94
- }
100
+ @inferred_settings_from_has_many ||= begin
101
+ return unless reflection.macro == :has_many
102
+ through_reflection = reflection.through_reflection
103
+ if through_reflection
104
+ {
105
+ joins: through_reflection.name,
106
+ column: "#{through_reflection.table_name}.#{reflection.source_reflection.foreign_key}"
107
+ }
108
+ else
109
+ {
110
+ joins: reflection.name,
111
+ column: "#{reflection.klass.table_name}.#{reflection.klass.primary_key}"
112
+ }
113
+ end
95
114
  end
96
115
  end
97
116
 
@@ -1,17 +1,22 @@
1
1
  module ReportsKit
2
2
  module Reports
3
- class Measure
4
- attr_accessor :properties, :filters, :context_record
3
+ class Measure < AbstractMeasure
4
+ attr_accessor :properties, :dimensions, :filters, :context_record
5
5
 
6
6
  def initialize(properties, context_record: nil)
7
+ properties = properties.dup
7
8
  properties = { key: properties } if properties.is_a?(String)
8
9
  raise ArgumentError.new("Measure properties must be a String or Hash, not a #{properties.class.name}: #{properties.inspect}") unless properties.is_a?(Hash)
9
10
  properties = properties.deep_symbolize_keys
10
- filter_hashes = properties.delete(:filters) || []
11
+
12
+ dimension_hashes = properties[:dimensions] || []
13
+ dimension_hashes = dimension_hashes.values if dimension_hashes.is_a?(Hash) && dimension_hashes.key?(:'0')
14
+ filter_hashes = properties[:filters] || []
11
15
  filter_hashes = filter_hashes.values if filter_hashes.is_a?(Hash) && filter_hashes.key?(:'0')
12
16
 
13
17
  self.properties = properties
14
- self.filters = filter_hashes.map { |filter_hash| Filter.new(filter_hash, measure: self) }
18
+ self.dimensions = dimension_hashes.map { |dimension_hash| DimensionWithMeasure.new(dimension: Dimension.new(dimension_hash), measure: self) }
19
+ self.filters = filter_hashes.map { |filter_hash| FilterWithMeasure.new(filter: Filter.new(filter_hash), measure: self) }
15
20
  self.context_record = context_record
16
21
  end
17
22
 
@@ -20,7 +25,7 @@ module ReportsKit
20
25
  end
21
26
 
22
27
  def label
23
- key.pluralize.titleize
28
+ properties[:name].presence || key.pluralize.titleize
24
29
  end
25
30
 
26
31
  def relation_name
@@ -28,11 +33,31 @@ module ReportsKit
28
33
  end
29
34
 
30
35
  def aggregate_function
31
- :count
36
+ aggregation_expression || :count
37
+ end
38
+
39
+ def aggregation_expression
40
+ return unless aggregation_config
41
+ expression = aggregation_config[:expression]
42
+ if expression.is_a?(Array)
43
+ expression
44
+ else
45
+ raise ArgumentError.new("The '#{aggregation_key}' aggregation on the #{model_class} model isn't valid")
46
+ end
32
47
  end
33
48
 
34
- def conditions
35
- nil
49
+ def aggregation_key
50
+ properties[:aggregation]
51
+ end
52
+
53
+ def aggregation_config
54
+ @aggregation_config ||= begin
55
+ return unless aggregation_key
56
+ raise ArgumentError.new("A '#{aggregation_key}' aggregation on the #{model_class} model hasn't been configured") unless model_class.respond_to?(:reports_kit_configuration)
57
+ config = model_class.reports_kit_configuration.aggregations.find { |aggregation| aggregation[:key] == aggregation_key }
58
+ raise ArgumentError.new("A '#{aggregation_key}' aggregation on the #{model_class} model hasn't been configured") unless config
59
+ config
60
+ end
36
61
  end
37
62
 
38
63
  def base_relation
@@ -52,10 +77,21 @@ module ReportsKit
52
77
  relation
53
78
  end
54
79
 
55
- def properties_with_filters
56
- all_properties = properties
57
- all_properties[:filters] = filters.map(&:properties)
58
- all_properties
80
+ def has_two_dimensions?
81
+ dimensions.length == 2
82
+ end
83
+
84
+ def self.new_from_properties!(properties, context_record:)
85
+ measure_hashes = [properties[:measure]].compact + Array(properties[:measures])
86
+ raise ArgumentError.new('At least one measure must be configured') if measure_hashes.blank?
87
+
88
+ measure_hashes.map do |measure_hash|
89
+ if measure_hash[:composite_operator].present?
90
+ CompositeMeasure.new(measure_hash)
91
+ else
92
+ new(measure_hash, context_record: context_record)
93
+ end
94
+ end
59
95
  end
60
96
  end
61
97
  end
@@ -1,9 +1,48 @@
1
+ require 'csv'
2
+ require 'spreadsheet'
3
+
1
4
  module ReportsKit
2
5
  class ReportsController < ReportsKit::BaseController
3
6
  def index
4
- properties = ActiveSupport::JSON.decode(params[:properties])
5
- report_data = Reports::Data::Generate.new(properties, context_record: context_record).perform
6
- render json: { data: report_data }
7
+ respond_to do |format|
8
+ format.json do
9
+ render json: { data: report_data }
10
+ end
11
+ format.csv do
12
+ properties[:format] = 'table'
13
+ csv = CSV.generate do |csv|
14
+ report_data[:table_data].each do |row|
15
+ csv << row
16
+ end
17
+ end
18
+ send_data csv, filename: "Report.csv"
19
+ end
20
+ format.xls do
21
+ properties[:format] = 'table'
22
+ send_data xls_string, filename: 'Report.xls', type: 'application/vnd.ms-excel'
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def report_data
30
+ Reports::Data::Generate.new(properties, context_record: context_record).perform
31
+ end
32
+
33
+ def properties
34
+ @properties ||= ActiveSupport::JSON.decode(params[:properties])
35
+ end
36
+
37
+ def xls_string
38
+ spreadsheet = Spreadsheet::Workbook.new
39
+ sheet = spreadsheet.create_worksheet
40
+ report_data[:table_data].each_with_index do |row, index|
41
+ sheet.update_row(index, *row)
42
+ end
43
+ io = StringIO.new
44
+ spreadsheet.write(io)
45
+ io.string
7
46
  end
8
47
  end
9
48
  end
@@ -1,3 +1,3 @@
1
1
  module ReportsKit
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -14,6 +14,7 @@ Gem::Specification.new do |s|
14
14
  s.license = 'MIT'
15
15
 
16
16
  s.add_dependency 'rails', '>= 3'
17
+ s.add_dependency 'spreadsheet', '>= 1.1'
17
18
 
18
19
  s.add_development_dependency 'appraisal'
19
20
  s.add_development_dependency 'rspec', '~> 3'
@@ -21,5 +22,6 @@ Gem::Specification.new do |s|
21
22
  s.add_development_dependency 'factory_girl', '~> 4'
22
23
  s.add_development_dependency 'pg', '>= 0.15'
23
24
  s.add_development_dependency 'pry', '~> 0'
25
+ s.add_development_dependency 'pry-byebug', '~> 1'
24
26
  s.add_development_dependency 'timecop', '~> 0'
25
27
  end
@@ -1,40 +1,165 @@
1
- - measure: issue
2
- dimensions:
3
- - repo
4
- - measure: issue
5
- dimensions:
6
- - repo
1
+ - measure:
2
+ key: issue
3
+ dimensions:
4
+ - repo
5
+ - measure:
6
+ key: issue
7
+ dimensions:
8
+ - repo
7
9
  chart:
8
10
  options:
9
11
  foo: bar
10
- - measure: issue
11
- dimensions:
12
- - repo
12
+ - measure:
13
+ key: issue
14
+ dimensions:
15
+ - repo
13
16
  chart:
14
17
  foo: bar
15
- - measure: issue
16
- dimensions:
17
- - repo
18
+ - measure:
19
+ key: issue
20
+ dimensions:
21
+ - repo
18
22
  chart:
19
23
  type: pie
20
24
  - measure:
21
25
  key: issue
22
- dimensions:
23
- - key: repo
26
+ dimensions:
27
+ - key: repo
24
28
  - measure:
25
29
  key: issue
26
- dimensions:
27
- - key: opened_at
30
+ dimensions:
31
+ - key: opened_at
28
32
  - measure:
29
33
  key: issue
30
- dimensions:
31
- - key: opened_at
32
- - key: repo
34
+ dimensions:
35
+ - key: opened_at
36
+ - key: repo
33
37
  - measure:
34
38
  key: issue
35
39
  filters:
36
40
  - key: locked
37
41
  - key: title
38
42
  - key: opened_at
39
- dimensions:
40
- - key: repo
43
+ dimensions:
44
+ - key: repo
45
+ - measures:
46
+ - key: issue
47
+ dimensions:
48
+ - repo
49
+ - key: label
50
+ dimensions:
51
+ - repo
52
+ - measures:
53
+ - key: issue
54
+ dimensions:
55
+ - repo
56
+ - key: label
57
+ dimensions:
58
+ - repo
59
+ - key: tag
60
+ dimensions:
61
+ - repo
62
+ - measures:
63
+ - key: issue
64
+ name: Repo Issues
65
+ dimensions:
66
+ - repo
67
+ - key: label
68
+ dimensions:
69
+ - repo
70
+ - measures:
71
+ - key: issue
72
+ name: Repo Issues
73
+ dimensions:
74
+ - key: repo
75
+ label:
76
+ - key: label
77
+ dimensions:
78
+ - repo
79
+ format: table
80
+ - measure:
81
+ key: label
82
+ dimensions:
83
+ - repo
84
+ order: dimension1.label
85
+ - measure:
86
+ key: label
87
+ dimensions:
88
+ - repo
89
+ order: dimension1.label desc
90
+ - measure:
91
+ key: label
92
+ dimensions:
93
+ - repo
94
+ order: count
95
+ - measure:
96
+ key: label
97
+ dimensions:
98
+ - repo
99
+ order: count desc
100
+ - measure:
101
+ key: issue
102
+ dimensions:
103
+ - opened_at
104
+ order: dimension1
105
+ - measure:
106
+ key: issue
107
+ dimensions:
108
+ - opened_at
109
+ order: dimension1 desc
110
+ - measure:
111
+ key: label
112
+ dimensions:
113
+ - repo
114
+ order: 0
115
+ - measure:
116
+ key: label
117
+ dimensions:
118
+ - repo
119
+ order: 0 desc
120
+ - measure:
121
+ key: label
122
+ dimensions:
123
+ - repo
124
+ order: 1
125
+ - measure:
126
+ key: label
127
+ dimensions:
128
+ - repo
129
+ order: 1 desc
130
+ - measure:
131
+ key: issue
132
+ dimensions:
133
+ - key: opened_at
134
+ - key: repo
135
+ order: dimension1
136
+ - measure:
137
+ key: issue
138
+ dimensions:
139
+ - key: opened_at
140
+ - key: repo
141
+ order: dimension1 desc
142
+ - measure:
143
+ key: label
144
+ dimensions:
145
+ - key: repo
146
+ - key: created_at
147
+ order: dimension1.label
148
+ - measure:
149
+ key: label
150
+ dimensions:
151
+ - key: repo
152
+ - key: created_at
153
+ order: dimension1.label desc
154
+ - measure:
155
+ key: issue
156
+ dimensions:
157
+ - key: opened_at
158
+ - key: repo
159
+ order: count
160
+ - measure:
161
+ key: issue
162
+ dimensions:
163
+ - key: opened_at
164
+ - key: repo
165
+ order: count desc