gitlab_internal_events_cli 0.0.1
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.tool-versions +1 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +90 -0
- data/LICENSE.txt +19 -0
- data/README.md +164 -0
- data/Rakefile +10 -0
- data/exe/gitlab-internal-events-cli +7 -0
- data/gitlab_internal_events_cli.gemspec +39 -0
- data/lib/gitlab_internal_events_cli/cli.rb +59 -0
- data/lib/gitlab_internal_events_cli/configuration.rb +115 -0
- data/lib/gitlab_internal_events_cli/event.rb +73 -0
- data/lib/gitlab_internal_events_cli/flows/event_definer.rb +306 -0
- data/lib/gitlab_internal_events_cli/flows/flow_advisor.rb +90 -0
- data/lib/gitlab_internal_events_cli/flows/metric_definer.rb +468 -0
- data/lib/gitlab_internal_events_cli/flows/usage_viewer.rb +474 -0
- data/lib/gitlab_internal_events_cli/gitlab_prompt.rb +9 -0
- data/lib/gitlab_internal_events_cli/global_state.rb +63 -0
- data/lib/gitlab_internal_events_cli/helpers/cli_inputs.rb +138 -0
- data/lib/gitlab_internal_events_cli/helpers/event_options.rb +63 -0
- data/lib/gitlab_internal_events_cli/helpers/files.rb +84 -0
- data/lib/gitlab_internal_events_cli/helpers/formatting.rb +166 -0
- data/lib/gitlab_internal_events_cli/helpers/group_ownership.rb +160 -0
- data/lib/gitlab_internal_events_cli/helpers/metric_options.rb +253 -0
- data/lib/gitlab_internal_events_cli/helpers/schema_loader.rb +25 -0
- data/lib/gitlab_internal_events_cli/helpers/service_ping_dashboards.rb +22 -0
- data/lib/gitlab_internal_events_cli/helpers.rb +47 -0
- data/lib/gitlab_internal_events_cli/http_cache.rb +52 -0
- data/lib/gitlab_internal_events_cli/metric.rb +406 -0
- data/lib/gitlab_internal_events_cli/schema_resolver.rb +25 -0
- data/lib/gitlab_internal_events_cli/subflows/database_metric_definer.rb +71 -0
- data/lib/gitlab_internal_events_cli/subflows/event_metric_definer.rb +258 -0
- data/lib/gitlab_internal_events_cli/text/event_definer.rb +166 -0
- data/lib/gitlab_internal_events_cli/text/flow_advisor.rb +64 -0
- data/lib/gitlab_internal_events_cli/text/metric_definer.rb +138 -0
- data/lib/gitlab_internal_events_cli/time_framed_key_path.rb +18 -0
- data/lib/gitlab_internal_events_cli/version.rb +5 -0
- data/lib/gitlab_internal_events_cli.rb +36 -0
- metadata +170 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Helpers related to listing existing event definitions
|
|
4
|
+
module GitlabInternalEventsCli
|
|
5
|
+
module Helpers
|
|
6
|
+
module EventOptions
|
|
7
|
+
def get_event_options(events)
|
|
8
|
+
options = events.filter_map do |(path, event)|
|
|
9
|
+
next if duplicate_events?(event.action, events.values)
|
|
10
|
+
|
|
11
|
+
description = format_help(" - #{trim_description(event.description)}")
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
name: "#{format_event_name(event)}#{description}",
|
|
15
|
+
value: path
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
options.sort_by do |option|
|
|
20
|
+
category = events.dig(option[:value], 'category')
|
|
21
|
+
internal_events = events.dig(option[:value], 'internal_events')
|
|
22
|
+
|
|
23
|
+
event_sort_param(internal_events, category, option[:name])
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def events_by_filepath(event_paths = [])
|
|
28
|
+
events = cli.global.events.to_h { |event| [event.file_path, event] }
|
|
29
|
+
|
|
30
|
+
return events if event_paths.none?
|
|
31
|
+
|
|
32
|
+
events.slice(*event_paths)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def trim_description(description)
|
|
38
|
+
return description if description.to_s.length < 50
|
|
39
|
+
|
|
40
|
+
"#{description[0, 50]}..."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def format_event_name(event)
|
|
44
|
+
if event.internal_events || event.category == 'default'
|
|
45
|
+
event.action
|
|
46
|
+
else
|
|
47
|
+
"#{event.category}:#{event.action}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def event_sort_param(internal_events, category, name)
|
|
52
|
+
return "0#{name}" if internal_events
|
|
53
|
+
return "1#{name}" if category == 'default'
|
|
54
|
+
|
|
55
|
+
"2#{category}#{name}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def duplicate_events?(action, events)
|
|
59
|
+
events.count { |event| action == event.action } > 1
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Helpers related reading/writing definition files
|
|
4
|
+
module GitlabInternalEventsCli
|
|
5
|
+
module Helpers
|
|
6
|
+
module Files
|
|
7
|
+
MAX_FILENAME_LENGTH = 100
|
|
8
|
+
|
|
9
|
+
def absolute_path(filepath)
|
|
10
|
+
GitlabInternalEventsCli.configuration.absolute_path(filepath)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def prompt_to_save_file(filepath, content)
|
|
14
|
+
full_path = absolute_path(filepath)
|
|
15
|
+
|
|
16
|
+
cli.say <<~TEXT.chomp
|
|
17
|
+
#{format_info('Preparing to generate definition with these attributes:')}
|
|
18
|
+
#{filepath}
|
|
19
|
+
#{content}
|
|
20
|
+
TEXT
|
|
21
|
+
|
|
22
|
+
if File.exist?(full_path)
|
|
23
|
+
cli.error("Oh no! This file already exists!\n")
|
|
24
|
+
|
|
25
|
+
return if cli.no?(format_prompt('Overwrite file?'))
|
|
26
|
+
|
|
27
|
+
write_to_file(filepath, content, 'update')
|
|
28
|
+
elsif cli.yes?(format_prompt('Create file?'))
|
|
29
|
+
write_to_file(filepath, content, 'create')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def file_saved_message(verb, filepath)
|
|
34
|
+
attributes = YAML.safe_load_file(absolute_path(filepath))
|
|
35
|
+
|
|
36
|
+
format_prefix ' ', [
|
|
37
|
+
file_saved_success_message(verb, filepath),
|
|
38
|
+
file_saved_context_message(attributes),
|
|
39
|
+
file_saved_validations_message(attributes)
|
|
40
|
+
].compact.join("\n")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def write_to_file(filepath, content, verb)
|
|
44
|
+
full_path = absolute_path(filepath)
|
|
45
|
+
dir = File.dirname(full_path)
|
|
46
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
47
|
+
|
|
48
|
+
File.write(full_path, content)
|
|
49
|
+
|
|
50
|
+
file_saved_message(verb, filepath).tap do |_message|
|
|
51
|
+
cli.global.reload_definitions
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def file_saved_success_message(verb, filepath)
|
|
58
|
+
"#{format_selection(verb)} #{filepath}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def file_saved_context_message(_attributes)
|
|
62
|
+
# Override in definition class
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def file_saved_validations_message(attributes)
|
|
66
|
+
schema = self.class.respond_to?(:schema) ? self.class.schema : nil
|
|
67
|
+
return '' unless schema
|
|
68
|
+
|
|
69
|
+
errors = schema.validate(attributes).to_a
|
|
70
|
+
|
|
71
|
+
return '' unless errors.any?
|
|
72
|
+
|
|
73
|
+
<<~TEXT
|
|
74
|
+
|
|
75
|
+
#{errors.map { |e| [format_warning('!! WARNING: '), JSONSchemer::Errors.pretty(e)].join }.join("\n")}
|
|
76
|
+
|
|
77
|
+
These errors will cause validation specs to fail and should be resolved before merging your changes.
|
|
78
|
+
TEXT
|
|
79
|
+
rescue StandardError
|
|
80
|
+
''
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Helpers related to visual formatting of outputs
|
|
4
|
+
module GitlabInternalEventsCli
|
|
5
|
+
module Helpers
|
|
6
|
+
module Formatting
|
|
7
|
+
DEFAULT_WINDOW_WIDTH = 100
|
|
8
|
+
DEFAULT_WINDOW_HEIGHT = 30
|
|
9
|
+
|
|
10
|
+
# When to format as "info":
|
|
11
|
+
# - When a header is needed to organize contextual
|
|
12
|
+
# information. These headers should always be all caps.
|
|
13
|
+
# - As a supplemental way to highlight the most important
|
|
14
|
+
# text within a menu or informational text.
|
|
15
|
+
# - Optionally, for URLs
|
|
16
|
+
def format_info(string)
|
|
17
|
+
pastel.cyan(string)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# When to format as "warning":
|
|
21
|
+
# - To highlight the first sentence/phrase describing a
|
|
22
|
+
# problem the user needs to address. Any further text
|
|
23
|
+
# explantion should be left unformatted.
|
|
24
|
+
# - To highlight an explanation of why the user cannot take
|
|
25
|
+
# a particular action.
|
|
26
|
+
def format_warning(string)
|
|
27
|
+
pastel.yellow(string)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# When to format as "selection":
|
|
31
|
+
# - As a supplemental way of indicating something was
|
|
32
|
+
# selected or the current state of an interaction.
|
|
33
|
+
def format_selection(string)
|
|
34
|
+
pastel.green(string)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# When to format as "help":
|
|
38
|
+
# - To format supplemental information on how to interact
|
|
39
|
+
# with prompts. This should always be in parenthesis.
|
|
40
|
+
# - To indicate disabled or unavailable menu options.
|
|
41
|
+
# - To indicate meta-information in menu options or
|
|
42
|
+
# informational text.
|
|
43
|
+
def format_help(string)
|
|
44
|
+
pastel.bright_black(string)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# When to format as "prompt":
|
|
48
|
+
# - When we need the user to input information. The text
|
|
49
|
+
# should describe the action the user should take to move
|
|
50
|
+
# forward, like `Input text` or `Select one`
|
|
51
|
+
# - As header text on multi-screen steps in a flow. Always
|
|
52
|
+
# include a counter when this is the case.
|
|
53
|
+
def format_prompt(string)
|
|
54
|
+
pastel.magenta(string)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# When to format as "error":
|
|
58
|
+
# - When the CLI encounters unexpected problems that may
|
|
59
|
+
# require broader changes by the Analytics Instrumentation
|
|
60
|
+
# Group or out of band configuration.
|
|
61
|
+
# - To highlight special characters used to symbolize that
|
|
62
|
+
# there was an error or that an option is not available.
|
|
63
|
+
def format_error(string)
|
|
64
|
+
pastel.red(string)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Strips all existing color/text style
|
|
68
|
+
def clear_format(string)
|
|
69
|
+
pastel.strip(string)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# When to format as "heading":
|
|
73
|
+
# - At the beginning or end of complete flows, to create
|
|
74
|
+
# visual separation and indicate logical breakpoints.
|
|
75
|
+
def format_heading(string)
|
|
76
|
+
[divider, pastel.cyan(string), divider].join("\n")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Used for grouping prompts that occur on the same screen
|
|
80
|
+
# or as part of the same step of a flow.
|
|
81
|
+
#
|
|
82
|
+
# Counter is exluded if total is 1.
|
|
83
|
+
# The subject's formatting is extended to the counter.
|
|
84
|
+
#
|
|
85
|
+
# @return [String] ex) -- EATING COOKIES (2/3): Chocolate Chip --
|
|
86
|
+
# @param subject [String] describes task generically ex) EATING COOKIES
|
|
87
|
+
# @param item [String] describes specific context ex) Chocolate Chip
|
|
88
|
+
# @param count [Integer] ex) 2
|
|
89
|
+
# @param total [Integer] ex) 3
|
|
90
|
+
def format_subheader(subject, item, count = 1, total = 1)
|
|
91
|
+
formatting_end = "\e[0m"
|
|
92
|
+
suffix = formatting_end if subject[-formatting_end.length..] == formatting_end
|
|
93
|
+
|
|
94
|
+
"-- #{[subject.chomp(formatting_end), counter(count, total)].compact.join(' ')}:#{suffix} #{item} --"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def format_prefix(prefix, string)
|
|
98
|
+
string.lines.map { |line| line.prepend(prefix) }.join
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# When to use a divider:
|
|
102
|
+
# - As separation between whole flows or format the layout
|
|
103
|
+
# of a screen or the layout of CLI outputs.
|
|
104
|
+
# - Dividers should not be used to differentiate between
|
|
105
|
+
# prompts on the same screen.
|
|
106
|
+
def divider
|
|
107
|
+
'-' * window_size
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Prints a progress bar on the screen at the current location
|
|
111
|
+
# @param current_title [String] title to highlight
|
|
112
|
+
# @param titles [Array<String>] progression to follow;
|
|
113
|
+
# -> first element is expected to be a title for the entire flow
|
|
114
|
+
def progress_bar(current_title, titles = [])
|
|
115
|
+
step = titles.index(current_title)
|
|
116
|
+
total = titles.length - 1
|
|
117
|
+
|
|
118
|
+
raise ArgumentError, "Invalid selection #{current_title} in progress bar" unless step
|
|
119
|
+
|
|
120
|
+
status = " Step #{step} / #{total} : #{titles.join(' > ')}"
|
|
121
|
+
status.gsub!(current_title, format_selection(current_title))
|
|
122
|
+
|
|
123
|
+
total_length = window_size - 4
|
|
124
|
+
step_length = step / total.to_f * total_length
|
|
125
|
+
|
|
126
|
+
incomplete = '-' * [(total_length - step_length - 1), 0].max
|
|
127
|
+
complete = '=' * [(step_length - 1), 0].max
|
|
128
|
+
|
|
129
|
+
"#{status}\n|==#{complete}>#{incomplete}|\n"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Formats a counter if there's anything to count
|
|
133
|
+
#
|
|
134
|
+
# @return [String, nil] ex) "(3/4)""
|
|
135
|
+
def counter(idx, total)
|
|
136
|
+
"(#{idx + 1}/#{total})" if total > 1
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def pastel
|
|
142
|
+
@pastel ||= Pastel.new
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def window_size
|
|
146
|
+
Integer(fetch_window_size)
|
|
147
|
+
rescue StandardError
|
|
148
|
+
DEFAULT_WINDOW_WIDTH
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def window_height
|
|
152
|
+
Integer(fetch_window_height)
|
|
153
|
+
rescue StandardError
|
|
154
|
+
DEFAULT_WINDOW_HEIGHT
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def fetch_window_size
|
|
158
|
+
`tput cols`
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def fetch_window_height
|
|
162
|
+
`tput lines`
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Helpers related to Stage/Section/Group ownership
|
|
4
|
+
module GitlabInternalEventsCli
|
|
5
|
+
module Helpers
|
|
6
|
+
module GroupOwnership
|
|
7
|
+
def known_categories
|
|
8
|
+
@known_categories ||= load_known_categories
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def prompt_for_group_ownership(message, defaults = {})
|
|
12
|
+
if available_groups.any?
|
|
13
|
+
prompt_for_ownership_from_ssot(message, defaults)
|
|
14
|
+
else
|
|
15
|
+
prompt_for_ownership_manually(message, defaults)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def prompt_for_feature_categories(message, groups, default = nil)
|
|
20
|
+
choices = category_choices(groups)
|
|
21
|
+
valid_defaults = Array(default).compact & known_categories
|
|
22
|
+
|
|
23
|
+
categories = cli.multi_select(
|
|
24
|
+
message,
|
|
25
|
+
choices,
|
|
26
|
+
**multiselect_opts,
|
|
27
|
+
**filter_opts(header_size: 10)
|
|
28
|
+
) do |menu|
|
|
29
|
+
menu.default(*valid_defaults) if valid_defaults.any?
|
|
30
|
+
format_disabled_options_as_dividers(menu)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
categories.sort if categories.any?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def find_stage(group)
|
|
37
|
+
available_groups[group]&.fetch(:stage)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def find_section(group)
|
|
41
|
+
available_groups[group]&.fetch(:section)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def find_categories(group)
|
|
45
|
+
available_groups[group]&.fetch(:categories)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def load_known_categories
|
|
51
|
+
# Try local file first, fall back to remote
|
|
52
|
+
path = GitlabInternalEventsCli.configuration.absolute_path(
|
|
53
|
+
GitlabInternalEventsCli.configuration.feature_categories_path
|
|
54
|
+
)
|
|
55
|
+
return YAML.load_file(path) if File.exist?(path)
|
|
56
|
+
|
|
57
|
+
# Fall back to fetching from remote
|
|
58
|
+
fetch_remote_categories
|
|
59
|
+
rescue StandardError
|
|
60
|
+
[]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def fetch_remote_categories
|
|
64
|
+
url = GitlabInternalEventsCli.configuration.feature_categories_url
|
|
65
|
+
response = HttpCache.get(url)
|
|
66
|
+
return [] unless response
|
|
67
|
+
|
|
68
|
+
YAML.safe_load(response)
|
|
69
|
+
rescue StandardError
|
|
70
|
+
[]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def prompt_for_ownership_from_ssot(prompt, defaults)
|
|
74
|
+
sorted_defaults = defaults.values_at(:section, :stage, :product_group)
|
|
75
|
+
group = sorted_defaults.last
|
|
76
|
+
default = sorted_defaults.compact.join(':') # compact because not all groups have a section
|
|
77
|
+
|
|
78
|
+
cli.select(prompt, group_choices, **select_opts, **filter_opts) do |menu|
|
|
79
|
+
if group
|
|
80
|
+
if available_groups[group]
|
|
81
|
+
# We have a complete group selection -> set as default in menu
|
|
82
|
+
menu.default(default)
|
|
83
|
+
else
|
|
84
|
+
cli.error format_error(">>> Failed to find group matching #{group}. Select another.\n")
|
|
85
|
+
end
|
|
86
|
+
elsif default
|
|
87
|
+
# We have a section and/or stage in common
|
|
88
|
+
menu.instance_variable_set(:@filter, default.chars)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def prompt_for_ownership_manually(message, defaults)
|
|
94
|
+
prompt_for_text(message, defaults[:product_group])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @return Array[<Hash - matches #prompt_for_ownership_manually output format>]
|
|
98
|
+
def group_choices
|
|
99
|
+
available_groups.map do |group, ownership|
|
|
100
|
+
{
|
|
101
|
+
name: ownership.values_at(:section, :stage, :group).compact.join(':'),
|
|
102
|
+
value: group
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Returns the list of product category options for use in
|
|
108
|
+
# select menu. Prioritizes categories from related groups.
|
|
109
|
+
def category_choices(groups)
|
|
110
|
+
options = []
|
|
111
|
+
|
|
112
|
+
# List likeliest categories for group as the first options
|
|
113
|
+
Array(groups).compact.uniq.each do |group|
|
|
114
|
+
divider = select_option_divider("Categories for #{group}")
|
|
115
|
+
categories = (find_categories(group) || []) & known_categories
|
|
116
|
+
|
|
117
|
+
options.push(divider, *categories.sort) if categories.any?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
divider = select_option_divider('All categories')
|
|
121
|
+
none_option = { name: 'N/A (this definition does not correspond to any product categories)', value: nil }
|
|
122
|
+
|
|
123
|
+
options.push(divider) if options.any?
|
|
124
|
+
options.concat(known_categories)
|
|
125
|
+
options.push(none_option)
|
|
126
|
+
options.uniq
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Output looks like:
|
|
130
|
+
# {
|
|
131
|
+
# "import" => { stage: "manage", section: "dev", group: "import" },
|
|
132
|
+
# ...
|
|
133
|
+
# }
|
|
134
|
+
def available_groups
|
|
135
|
+
return @available_groups if defined?(@available_groups)
|
|
136
|
+
|
|
137
|
+
response = HttpCache.get(GitlabInternalEventsCli.configuration.stages_url)
|
|
138
|
+
return @available_groups = {} unless response
|
|
139
|
+
|
|
140
|
+
data = YAML.safe_load(response)
|
|
141
|
+
|
|
142
|
+
@available_groups = data['stages'].flat_map do |stage_name, stage_data|
|
|
143
|
+
stage_data['groups'].map do |group_name, group_data|
|
|
144
|
+
[
|
|
145
|
+
group_name,
|
|
146
|
+
{
|
|
147
|
+
group: group_name,
|
|
148
|
+
stage: stage_name,
|
|
149
|
+
section: stage_data['section'],
|
|
150
|
+
categories: group_data['categories']
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
end
|
|
154
|
+
end.to_h
|
|
155
|
+
rescue StandardError
|
|
156
|
+
@available_groups = {}
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|