reports_kit 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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