morpheus-cli 5.0.0 → 5.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 +4 -4
- data/Dockerfile +1 -1
- data/lib/morpheus/api/api_client.rb +12 -0
- data/lib/morpheus/api/billing_interface.rb +1 -0
- data/lib/morpheus/api/deploy_interface.rb +1 -1
- data/lib/morpheus/api/deployments_interface.rb +20 -1
- data/lib/morpheus/api/forgot_password_interface.rb +17 -0
- data/lib/morpheus/api/instances_interface.rb +7 -0
- data/lib/morpheus/api/search_interface.rb +13 -0
- data/lib/morpheus/api/servers_interface.rb +7 -0
- data/lib/morpheus/api/usage_interface.rb +18 -0
- data/lib/morpheus/cli.rb +4 -1
- data/lib/morpheus/cli/cli_command.rb +26 -9
- data/lib/morpheus/cli/commands/standard/curl_command.rb +3 -5
- data/lib/morpheus/cli/commands/standard/history_command.rb +3 -1
- data/lib/morpheus/cli/commands/standard/man_command.rb +74 -40
- data/lib/morpheus/cli/deploy.rb +199 -90
- data/lib/morpheus/cli/deployments.rb +341 -28
- data/lib/morpheus/cli/deploys.rb +206 -41
- data/lib/morpheus/cli/error_handler.rb +7 -0
- data/lib/morpheus/cli/forgot_password.rb +133 -0
- data/lib/morpheus/cli/health_command.rb +2 -2
- data/lib/morpheus/cli/hosts.rb +169 -32
- data/lib/morpheus/cli/instances.rb +83 -32
- data/lib/morpheus/cli/invoices_command.rb +33 -16
- data/lib/morpheus/cli/logs_command.rb +9 -6
- data/lib/morpheus/cli/mixins/deployments_helper.rb +31 -2
- data/lib/morpheus/cli/mixins/print_helper.rb +0 -21
- data/lib/morpheus/cli/mixins/provisioning_helper.rb +24 -4
- data/lib/morpheus/cli/option_types.rb +266 -17
- data/lib/morpheus/cli/remote.rb +35 -10
- data/lib/morpheus/cli/reports_command.rb +99 -30
- data/lib/morpheus/cli/search_command.rb +182 -0
- data/lib/morpheus/cli/setup.rb +1 -1
- data/lib/morpheus/cli/shell.rb +33 -11
- data/lib/morpheus/cli/tasks.rb +20 -21
- data/lib/morpheus/cli/usage_command.rb +64 -11
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/cli/virtual_images.rb +280 -199
- data/lib/morpheus/cli/whoami.rb +6 -6
- data/lib/morpheus/cli/workflows.rb +33 -40
- data/lib/morpheus/formatters.rb +22 -0
- data/lib/morpheus/terminal.rb +6 -2
- metadata +7 -2
@@ -25,16 +25,19 @@ class Morpheus::Cli::InvoicesCommand
|
|
25
25
|
options = {}
|
26
26
|
params = {}
|
27
27
|
ref_ids = []
|
28
|
-
query_tags = {}
|
29
28
|
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
30
29
|
opts.banner = subcommand_usage()
|
31
30
|
opts.on('-a', '--all', "Display all details, costs and prices." ) do
|
32
31
|
options[:show_all] = true
|
32
|
+
options[:show_dates] = true
|
33
33
|
options[:show_estimates] = true
|
34
34
|
# options[:show_costs] = true
|
35
35
|
options[:show_prices] = true
|
36
36
|
# options[:show_raw_data] = true
|
37
37
|
end
|
38
|
+
opts.on('--dates', "Display Ref Start, Ref End, etc.") do |val|
|
39
|
+
options[:show_dates] = true
|
40
|
+
end
|
38
41
|
opts.on('--estimates', '--estimates', "Display all estimated costs, from usage info: Compute, Storage, Network, Extra" ) do
|
39
42
|
options[:show_estimates] = true
|
40
43
|
end
|
@@ -107,9 +110,12 @@ class Morpheus::Cli::InvoicesCommand
|
|
107
110
|
params['accountId'] = val
|
108
111
|
end
|
109
112
|
opts.on('--tags Name=Value',String, "Filter by tags.") do |val|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
+
val.split(",").each do |value_pair|
|
114
|
+
k,v = value_pair.strip.split("=")
|
115
|
+
options[:tags] ||= {}
|
116
|
+
options[:tags][k] ||= []
|
117
|
+
options[:tags][k] << (v || '')
|
118
|
+
end
|
113
119
|
end
|
114
120
|
opts.on('--raw-data', '--raw-data', "Display Raw Data, the cost data from the cloud provider's API.") do |val|
|
115
121
|
options[:show_raw_data] = true
|
@@ -169,8 +175,8 @@ class Morpheus::Cli::InvoicesCommand
|
|
169
175
|
end
|
170
176
|
params['rawData'] = true if options[:show_raw_data]
|
171
177
|
params['refId'] = ref_ids unless ref_ids.empty?
|
172
|
-
if
|
173
|
-
|
178
|
+
if options[:tags] && !options[:tags].empty?
|
179
|
+
options[:tags].each do |k,v|
|
174
180
|
params['tags.' + k] = v
|
175
181
|
end
|
176
182
|
end
|
@@ -203,6 +209,7 @@ class Morpheus::Cli::InvoicesCommand
|
|
203
209
|
{"INVOICE ID" => lambda {|it| it['id'] } },
|
204
210
|
{"TYPE" => lambda {|it| format_invoice_ref_type(it) } },
|
205
211
|
{"REF ID" => lambda {|it| it['refId'] } },
|
212
|
+
{"REF UUID" => lambda {|it| it['refUuid'] } },
|
206
213
|
{"REF NAME" => lambda {|it|
|
207
214
|
if options[:show_all]
|
208
215
|
it['refName']
|
@@ -218,9 +225,11 @@ class Morpheus::Cli::InvoicesCommand
|
|
218
225
|
{"PERIOD" => lambda {|it| format_invoice_period(it) } },
|
219
226
|
{"START" => lambda {|it| format_date(it['startDate']) } },
|
220
227
|
{"END" => lambda {|it| format_date(it['endDate']) } },
|
221
|
-
] + (options[:show_all] ? [
|
228
|
+
] + ((options[:show_dates] || options[:show_all]) ? [
|
222
229
|
{"REF START" => lambda {|it| format_dt(it['refStart']) } },
|
223
230
|
{"REF END" => lambda {|it| format_dt(it['refEnd']) } },
|
231
|
+
# {"LAST COST DATE" => lambda {|it| format_local_dt(it['lastCostDate']) } },
|
232
|
+
# {"LAST ACTUAL DATE" => lambda {|it| format_local_dt(it['lastActualDate']) } },
|
224
233
|
] : []) + [
|
225
234
|
{"COMPUTE" => lambda {|it| format_money(it['computeCost'], 'usd', {sigdig:options[:sigdig]}) } },
|
226
235
|
# {"MEMORY" => lambda {|it| format_money(it['memoryCost']) } },
|
@@ -272,9 +281,15 @@ class Morpheus::Cli::InvoicesCommand
|
|
272
281
|
{"PROJECT TAGS" => lambda {|it| it['project'] ? truncate_string(format_metadata(it['project']['tags']), 50) : '' } },
|
273
282
|
]
|
274
283
|
end
|
284
|
+
if options[:show_dates]
|
285
|
+
columns += [
|
286
|
+
{"LAST COST DATE" => lambda {|it| format_local_dt(it['lastCostDate']) } },
|
287
|
+
{"LAST ACTUAL DATE" => lambda {|it| format_local_dt(it['lastActualDate']) } },
|
288
|
+
]
|
289
|
+
end
|
275
290
|
columns += [
|
276
291
|
{"CREATED" => lambda {|it| format_local_dt(it['dateCreated']) } },
|
277
|
-
{"UPDATED" => lambda {|it| format_local_dt(it['lastUpdated']) } }
|
292
|
+
{"UPDATED" => lambda {|it| format_local_dt(it['lastUpdated']) } },
|
278
293
|
]
|
279
294
|
if options[:show_raw_data]
|
280
295
|
columns += [{"RAW DATA" => lambda {|it| truncate_string(it['rawData'].to_s, 10) } }]
|
@@ -656,7 +671,6 @@ EOT
|
|
656
671
|
options = {}
|
657
672
|
params = {}
|
658
673
|
ref_ids = []
|
659
|
-
query_tags = {}
|
660
674
|
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
661
675
|
opts.banner = subcommand_usage()
|
662
676
|
opts.on('-a', '--all', "Display all details, costs and prices." ) do
|
@@ -744,11 +758,14 @@ EOT
|
|
744
758
|
opts.on('--tenant ID', String, "View invoice line items for a tenant. Default is your own account.") do |val|
|
745
759
|
params['accountId'] = val
|
746
760
|
end
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
761
|
+
opts.on('--tags Name=Value',String, "Filter by tags.") do |val|
|
762
|
+
val.split(",").each do |value_pair|
|
763
|
+
k,v = value_pair.strip.split("=")
|
764
|
+
options[:tags] ||= {}
|
765
|
+
options[:tags][k] ||= []
|
766
|
+
options[:tags][k] << (v || '')
|
767
|
+
end
|
768
|
+
end
|
752
769
|
opts.on('--raw-data', '--raw-data', "Display Raw Data, the cost data from the cloud provider's API.") do |val|
|
753
770
|
options[:show_raw_data] = true
|
754
771
|
end
|
@@ -808,8 +825,8 @@ EOT
|
|
808
825
|
end
|
809
826
|
params['rawData'] = true if options[:show_raw_data]
|
810
827
|
params['refId'] = ref_ids unless ref_ids.empty?
|
811
|
-
if
|
812
|
-
|
828
|
+
if options[:tags] && !options[:tags].empty?
|
829
|
+
options[:tags].each do |k,v|
|
813
830
|
params['tags.' + k] = v
|
814
831
|
end
|
815
832
|
end
|
@@ -34,7 +34,7 @@ class Morpheus::Cli::LogsCommand
|
|
34
34
|
options = {}
|
35
35
|
params = {}
|
36
36
|
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
37
|
-
opts.banner = subcommand_usage("[
|
37
|
+
opts.banner = subcommand_usage("[search]")
|
38
38
|
opts.on('--hosts HOSTS', String, "Filter logs to specific Host ID(s)") do |val|
|
39
39
|
params['servers'] = val.to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
|
40
40
|
end
|
@@ -72,18 +72,21 @@ class Morpheus::Cli::LogsCommand
|
|
72
72
|
options[:details] = true
|
73
73
|
end
|
74
74
|
build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
|
75
|
-
opts.footer = "List logs for
|
76
|
-
"[id] is required. This is the id of a container."
|
75
|
+
opts.footer = "List logs for all hosts and containers."
|
77
76
|
end
|
78
77
|
optparse.parse!(args)
|
79
|
-
if args.count
|
80
|
-
|
78
|
+
if args.count > 0
|
79
|
+
options[:phrase] = args.join(" ")
|
81
80
|
end
|
82
81
|
connect(options)
|
83
82
|
begin
|
84
83
|
params['level'] = params['level'].collect {|it| it.to_s.upcase }.join('|') if params['level'] # api works with INFO|WARN
|
85
84
|
params.merge!(parse_list_options(options))
|
86
|
-
|
85
|
+
if params['phrase']
|
86
|
+
options.delete(:phrase)
|
87
|
+
search_phrase = params.delete('phrase')
|
88
|
+
params['query'] = search_phrase
|
89
|
+
end
|
87
90
|
params['order'] = params['direction'] unless params['direction'].nil? # old api version expects order instead of direction
|
88
91
|
params['startMs'] = (options[:start].to_i * 1000) if options[:start]
|
89
92
|
params['endMs'] = (options[:end].to_i * 1000) if options[:end]
|
@@ -51,9 +51,10 @@ module Morpheus::Cli::DeploymentsHelper
|
|
51
51
|
return nil
|
52
52
|
elsif deployments.size > 1
|
53
53
|
print_red_alert "#{deployments.size} deployments found by name '#{name}'"
|
54
|
+
print_error "\n"
|
54
55
|
puts_error as_pretty_table(deployments, [:id, :name], {color:red})
|
55
56
|
print_red_alert "Try using ID instead"
|
56
|
-
|
57
|
+
print_error reset,"\n"
|
57
58
|
return nil
|
58
59
|
else
|
59
60
|
return deployments[0]
|
@@ -122,13 +123,41 @@ module Morpheus::Cli::DeploymentsHelper
|
|
122
123
|
return nil
|
123
124
|
elsif deployment_versions.size > 1
|
124
125
|
print_red_alert "#{deployment_versions.size} deployment versions found by version '#{name}'"
|
126
|
+
print_error "\n"
|
125
127
|
puts_error as_pretty_table(deployment_versions, {"ID" => 'id', "VERSION" => 'userVersion'}, {color:red})
|
126
128
|
print_red_alert "Try using ID instead"
|
127
|
-
|
129
|
+
print_error reset,"\n"
|
128
130
|
return nil
|
129
131
|
else
|
130
132
|
return deployment_versions[0]
|
131
133
|
end
|
132
134
|
end
|
133
135
|
|
136
|
+
def format_deployment_version_number(deployment_version)
|
137
|
+
if deployment_version
|
138
|
+
deployment_version['userVersion'] || deployment_version['version'] || ''
|
139
|
+
else
|
140
|
+
''
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def format_app_deploy_status(status, return_color=cyan)
|
145
|
+
out = ""
|
146
|
+
s = status.to_s.downcase
|
147
|
+
if s == 'deployed' || s == 'committed'
|
148
|
+
out << "#{green}#{s.upcase}#{return_color}"
|
149
|
+
elsif s == 'open' || s == 'archived'
|
150
|
+
out << "#{cyan}#{s.upcase}#{return_color}"
|
151
|
+
elsif s == 'failed'
|
152
|
+
out << "#{red}#{s.upcase}#{return_color}"
|
153
|
+
else
|
154
|
+
out << "#{yellow}#{s.upcase}#{return_color}"
|
155
|
+
end
|
156
|
+
out
|
157
|
+
end
|
158
|
+
|
159
|
+
def format_deploy_type(val)
|
160
|
+
return val
|
161
|
+
end
|
162
|
+
|
134
163
|
end
|
@@ -1151,27 +1151,6 @@ module Morpheus::Cli::PrintHelper
|
|
1151
1151
|
out
|
1152
1152
|
end
|
1153
1153
|
|
1154
|
-
def format_list(items, conjunction="and", limit=nil)
|
1155
|
-
items = items ? items.clone : []
|
1156
|
-
if limit
|
1157
|
-
items = items.first(limit)
|
1158
|
-
end
|
1159
|
-
last_item = items.pop
|
1160
|
-
if items.empty?
|
1161
|
-
return "#{last_item}"
|
1162
|
-
else
|
1163
|
-
return items.join(", ") + (conjunction.to_s.empty? ? ", " : " #{conjunction} ") + "#{last_item}" + ((limit && limit < (items.size+1)) ? " ..." : "")
|
1164
|
-
end
|
1165
|
-
end
|
1166
|
-
|
1167
|
-
def anded_list(items, limit=nil)
|
1168
|
-
format_list(items, "and", limit)
|
1169
|
-
end
|
1170
|
-
|
1171
|
-
def ored_list(items, limit=nil)
|
1172
|
-
format_list(items, "or", limit)
|
1173
|
-
end
|
1174
|
-
|
1175
1154
|
def sleep_with_dots(sleep_seconds, dots=3, dot_chr=".")
|
1176
1155
|
dot_interval = (sleep_seconds.to_f / dots.to_i)
|
1177
1156
|
dots.to_i.times do |dot_index|
|
@@ -596,11 +596,13 @@ module Morpheus::Cli::ProvisioningHelper
|
|
596
596
|
v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'environment', 'fieldLabel' => 'Environment', 'type' => 'select', 'required' => false, 'selectOptions' => get_available_environments()}], options[:options])
|
597
597
|
payload['instance']['instanceContext'] = v_prompt['environment'] if !v_prompt['environment'].empty?
|
598
598
|
|
599
|
-
# Labels (tags)
|
600
|
-
|
601
|
-
|
599
|
+
# Labels (Provisioning API still refers to these as tags)
|
600
|
+
# and tags (metadata tags) is called metadata.
|
601
|
+
# todo: switch this from 'tags' to labels' when the api changes
|
602
|
+
if options[:labels]
|
603
|
+
payload['instance']['tags'] = options[:labels].is_a?(Array) ? options[:labels] : options[:labels].to_s.split(',').collect {|it| it.to_s.strip }.compact.uniq
|
602
604
|
else
|
603
|
-
v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => '
|
605
|
+
v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'labels', 'fieldLabel' => 'Labels', 'type' => 'text', 'required' => false}], options[:options])
|
604
606
|
payload['instance']['tags'] = v_prompt['tags'].split(',').collect {|it| it.to_s.strip }.compact.uniq if !v_prompt['tags'].empty?
|
605
607
|
end
|
606
608
|
|
@@ -976,6 +978,9 @@ module Morpheus::Cli::ProvisioningHelper
|
|
976
978
|
metadata_list = options[:metadata].split(",").select {|it| !it.to_s.empty? }
|
977
979
|
metadata_list = metadata_list.collect do |it|
|
978
980
|
metadata_pair = it.split(":")
|
981
|
+
if metadata_pair.size < 2 && it.include?("=")
|
982
|
+
metadata_pair = it.split("=")
|
983
|
+
end
|
979
984
|
row = {}
|
980
985
|
row['name'] = metadata_pair[0].to_s.strip
|
981
986
|
row['value'] = metadata_pair[1].to_s.strip
|
@@ -2200,4 +2205,19 @@ module Morpheus::Cli::ProvisioningHelper
|
|
2200
2205
|
return type_code.to_s.downcase
|
2201
2206
|
end
|
2202
2207
|
end
|
2208
|
+
|
2209
|
+
def format_snapshot_status(snapshot, return_color=cyan)
|
2210
|
+
out = ""
|
2211
|
+
status_string = snapshot['status'].to_s
|
2212
|
+
if status_string == 'complete'
|
2213
|
+
out << "#{green}#{status_string.upcase}#{return_color}"
|
2214
|
+
elsif status_string == 'creating'
|
2215
|
+
out << "#{cyan}#{status_string.upcase}#{return_color}"
|
2216
|
+
elsif status_string == 'failed'
|
2217
|
+
out << "#{red}#{status_string.upcase}#{return_color}"
|
2218
|
+
else
|
2219
|
+
out << "#{yellow}#{status_string.upcase}#{return_color}"
|
2220
|
+
end
|
2221
|
+
out
|
2222
|
+
end
|
2203
2223
|
end
|
@@ -5,6 +5,7 @@ module Morpheus
|
|
5
5
|
module Cli
|
6
6
|
module OptionTypes
|
7
7
|
include Term::ANSIColor
|
8
|
+
# include Morpheus::Cli::PrintHelper
|
8
9
|
|
9
10
|
def self.confirm(message,options={})
|
10
11
|
if options[:yes] == true
|
@@ -72,9 +73,15 @@ module Morpheus
|
|
72
73
|
field_name = namespaces.pop
|
73
74
|
|
74
75
|
# respect optionType.dependsOnCode
|
75
|
-
|
76
|
+
# i guess this switched to visibleOnCode, respect one or the other
|
77
|
+
visible_option_check_value = option_type['dependsOnCode']
|
78
|
+
if !option_type['visibleOnCode'].to_s.empty?
|
79
|
+
visible_option_check_value = option_type['visibleOnCode']
|
80
|
+
end
|
81
|
+
if !visible_option_check_value.to_s.empty?
|
76
82
|
# support formats code=value or code:value OR code:(value|value2|value3)
|
77
|
-
|
83
|
+
# OR fieldContext.fieldName=value
|
84
|
+
parts = visible_option_check_value.include?("=") ? visible_option_check_value.split("=") : visible_option_check_value.split(":")
|
78
85
|
depends_on_code = parts[0]
|
79
86
|
depends_on_value = parts[1].to_s.strip
|
80
87
|
depends_on_values = []
|
@@ -87,14 +94,29 @@ module Morpheus
|
|
87
94
|
depends_on_values = depends_on_value.split("|").collect { |it| it.strip }
|
88
95
|
end
|
89
96
|
depends_on_option_type = option_types.find {|it| it["code"] == depends_on_code }
|
90
|
-
|
91
|
-
|
97
|
+
if !depends_on_option_type
|
98
|
+
depends_on_option_type = option_types.find {|it|
|
99
|
+
(it['fieldContext'] ? "#{it['fieldContext']}.#{it['fieldName']}" : it['fieldName']) == depends_on_code
|
100
|
+
}
|
101
|
+
end
|
102
|
+
if depends_on_option_type
|
92
103
|
# dependent option type has a different value
|
93
104
|
depends_on_field_key = depends_on_option_type['fieldContext'] ? "#{depends_on_option_type['fieldContext']}.#{depends_on_option_type['fieldName']}" : "#{depends_on_option_type['fieldName']}"
|
94
105
|
found_dep_value = get_object_value(results, depends_on_field_key) || get_object_value(options, depends_on_field_key)
|
95
|
-
if depends_on_values.size > 0
|
96
|
-
|
106
|
+
if depends_on_values.size > 0
|
107
|
+
# must be in the specified values
|
108
|
+
# todo: uhh this actually needs to change to parse regex
|
109
|
+
if !depends_on_values.include?(found_dep_value)
|
110
|
+
next
|
111
|
+
end
|
112
|
+
else
|
113
|
+
# no value found
|
114
|
+
if found_dep_value.to_s.empty?
|
115
|
+
next
|
116
|
+
end
|
97
117
|
end
|
118
|
+
else
|
119
|
+
# could not find the dependent option type, proceed and prompt
|
98
120
|
end
|
99
121
|
end
|
100
122
|
|
@@ -115,7 +137,7 @@ module Morpheus
|
|
115
137
|
# use the value passed in the options map
|
116
138
|
if cur_namespace.respond_to?('key?') && cur_namespace.key?(field_name)
|
117
139
|
value = cur_namespace[field_name]
|
118
|
-
input_value = ['select', 'multiSelect'].include?(option_type['type']) && option_type['fieldInput'] ? cur_namespace[option_type['fieldInput']] : nil
|
140
|
+
input_value = ['select', 'multiSelect','typeahead', 'multiTypeahead'].include?(option_type['type']) && option_type['fieldInput'] ? cur_namespace[option_type['fieldInput']] : nil
|
119
141
|
if option_type['type'] == 'number'
|
120
142
|
value = value.to_s.include?('.') ? value.to_f : value.to_i
|
121
143
|
# these select prompts should just fall down through below, with the extra params no_prompt, use_value
|
@@ -130,7 +152,17 @@ module Morpheus
|
|
130
152
|
select_value_list << select_prompt(option_type.merge({'defaultValue' => v, 'defaultInputValue' => input_value_list[i]}), api_client, (api_params || {}).merge(results), true)
|
131
153
|
end
|
132
154
|
value = select_value_list
|
133
|
-
|
155
|
+
elsif option_type['type'] == 'typeahead'
|
156
|
+
value = typeahead_prompt(option_type.merge({'defaultValue' => value, 'defaultInputValue' => input_value}), api_client, (api_params || {}).merge(results), true)
|
157
|
+
elsif option_type['type'] == 'multiTypeahead'
|
158
|
+
# support value as csv like "thing1, thing2"
|
159
|
+
value_list = value.is_a?(String) ? value.parse_csv.collect {|v| v ? v.to_s.strip : v } : [value].flatten
|
160
|
+
input_value_list = input_value.is_a?(String) ? input_value.parse_csv.collect {|v| v ? v.to_s.strip : v } : [input_value].flatten
|
161
|
+
select_value_list = []
|
162
|
+
value_list.each_with_index do |v, i|
|
163
|
+
select_value_list << typeahead_prompt(option_type.merge({'defaultValue' => v, 'defaultInputValue' => input_value_list[i]}), api_client, (api_params || {}).merge(results), true)
|
164
|
+
end
|
165
|
+
value = select_value_list
|
134
166
|
end
|
135
167
|
if options[:always_prompt] != true
|
136
168
|
value_found = true
|
@@ -147,7 +179,7 @@ module Morpheus
|
|
147
179
|
no_prompt = no_prompt || options[:no_prompt]
|
148
180
|
if no_prompt
|
149
181
|
if !value_found
|
150
|
-
if option_type['defaultValue'] != nil && !['select', 'multiSelect'].include?(option_type['type'])
|
182
|
+
if option_type['defaultValue'] != nil && !['select', 'multiSelect','typeahead','multiTypeahead'].include?(option_type['type'])
|
151
183
|
value = option_type['defaultValue']
|
152
184
|
value_found = true
|
153
185
|
end
|
@@ -158,6 +190,10 @@ module Morpheus
|
|
158
190
|
value = select_prompt(option_type, api_client, (api_params || {}).merge(results), true)
|
159
191
|
value_found = !!value
|
160
192
|
end
|
193
|
+
if ['typeahead', 'multiTypeahead'].include?(option_type['type'])
|
194
|
+
value = typeahead_prompt(option_type, api_client, (api_params || {}).merge(results), true)
|
195
|
+
value_found = !!value
|
196
|
+
end
|
161
197
|
if !value_found
|
162
198
|
if option_type['required']
|
163
199
|
print Term::ANSIColor.red, "\nMissing Required Option\n\n", Term::ANSIColor.reset
|
@@ -203,6 +239,18 @@ module Morpheus
|
|
203
239
|
end
|
204
240
|
end
|
205
241
|
end
|
242
|
+
elsif ['typeahead', 'multiTypeahead'].include?(option_type['type'])
|
243
|
+
value = typeahead_prompt(option_type, api_client, (api_params || {}).merge(results), options[:no_prompt], nil, paging_enabled)
|
244
|
+
if value && option_type['type'] == 'multiTypeahead'
|
245
|
+
value = [value]
|
246
|
+
while self.confirm("Add another #{option_type['fieldLabel']}?", {:default => false}) do
|
247
|
+
if addn_value = typeahead_prompt(option_type, api_client, (api_params || {}).merge(results), options[:no_prompt], nil, paging_enabled)
|
248
|
+
value << addn_value
|
249
|
+
else
|
250
|
+
break
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
206
254
|
elsif option_type['type'] == 'hidden'
|
207
255
|
value = option_type['defaultValue']
|
208
256
|
input = value
|
@@ -310,11 +358,12 @@ module Morpheus
|
|
310
358
|
value_field = (option_type['config'] ? option_type['config']['valueField'] : nil) || 'value'
|
311
359
|
default_value = option_type['defaultValue']
|
312
360
|
default_value = default_value['id'] if default_value && default_value.is_a?(Hash) && !default_value['id'].nil?
|
361
|
+
api_params ||= {}
|
313
362
|
# local array of options
|
314
363
|
if option_type['selectOptions']
|
315
364
|
# calculate from inline lambda
|
316
365
|
if option_type['selectOptions'].is_a?(Proc)
|
317
|
-
select_options = option_type['selectOptions'].call()
|
366
|
+
select_options = option_type['selectOptions'].call(api_client, grails_params(api_params || {}))
|
318
367
|
else
|
319
368
|
# todo: better type validation
|
320
369
|
select_options = option_type['selectOptions']
|
@@ -325,7 +374,7 @@ module Morpheus
|
|
325
374
|
select_options = option_type['optionSource'].call(api_client, grails_params(api_params || {}))
|
326
375
|
elsif option_type['optionSource'] == 'list'
|
327
376
|
# /api/options/list is a special action for custom OptionTypeLists, just need to pass the optionTypeId parameter
|
328
|
-
select_options = load_source_options(option_type['optionSource'], api_client, {'optionTypeId' => option_type['id']})
|
377
|
+
select_options = load_source_options(option_type['optionSource'], api_client, grails_params(api_params || {}).merge({'optionTypeId' => option_type['id']}))
|
329
378
|
else
|
330
379
|
# remote optionSource aka /api/options/$optionSource?
|
331
380
|
select_options = load_source_options(option_type['optionSource'], api_client, grails_params(api_params || {}))
|
@@ -345,7 +394,7 @@ module Morpheus
|
|
345
394
|
print Term::ANSIColor.red, " * #{option_type['fieldLabel']} [-O #{option_type['fieldContext'] ? (option_type['fieldContext']+'.') : ''}#{option_type['fieldName']}=] - #{option_type['description']}\n", Term::ANSIColor.reset
|
346
395
|
if select_options && select_options.size > 10
|
347
396
|
display_select_options(option_type, select_options.first(10))
|
348
|
-
puts " (#{select_options.size-
|
397
|
+
puts " (#{select_options.size-10} more)"
|
349
398
|
else
|
350
399
|
display_select_options(option_type, select_options)
|
351
400
|
end
|
@@ -388,7 +437,7 @@ module Morpheus
|
|
388
437
|
print Term::ANSIColor.red, " * #{option_type['fieldLabel']} [-O #{help_field_key}=] - #{option_type['description']}\n", Term::ANSIColor.reset
|
389
438
|
if select_options && select_options.size > 10
|
390
439
|
display_select_options(option_type, select_options.first(10))
|
391
|
-
puts " (#{select_options.size-
|
440
|
+
puts " (#{select_options.size-10} more)"
|
392
441
|
else
|
393
442
|
display_select_options(option_type, select_options)
|
394
443
|
end
|
@@ -458,6 +507,154 @@ module Morpheus
|
|
458
507
|
value
|
459
508
|
end
|
460
509
|
|
510
|
+
# this works like select_prompt, but refreshes options with ?query=value between inputs
|
511
|
+
# paging_enabled is ignored right now
|
512
|
+
def self.typeahead_prompt(option_type,api_client, api_params={}, no_prompt=false, use_value=nil, paging_enabled=false)
|
513
|
+
select_options = []
|
514
|
+
field_key = [option_type['fieldContext'], option_type['fieldName']].select {|it| it && it != '' }.join('.')
|
515
|
+
help_field_key = option_type[:help_field_prefix] ? "#{option_type[:help_field_prefix]}.#{field_key}" : field_key
|
516
|
+
input = ""
|
517
|
+
value_found = false
|
518
|
+
value = nil
|
519
|
+
value_field = (option_type['config'] ? option_type['config']['valueField'] : nil) || 'value'
|
520
|
+
default_value = option_type['defaultValue']
|
521
|
+
default_value = default_value['id'] if default_value && default_value.is_a?(Hash) && !default_value['id'].nil?
|
522
|
+
|
523
|
+
while !value_found do
|
524
|
+
# ok get input, refresh options and see if it matches
|
525
|
+
# if matches one, cool otherwise print matches and reprompt or error
|
526
|
+
if use_value
|
527
|
+
input = use_value
|
528
|
+
elsif no_prompt
|
529
|
+
input = default_value
|
530
|
+
else
|
531
|
+
Readline.completion_append_character = ""
|
532
|
+
Readline.basic_word_break_characters = ''
|
533
|
+
Readline.completion_proc = proc {|s|
|
534
|
+
matches = []
|
535
|
+
available_options = (select_options || [])
|
536
|
+
available_options.each{|option|
|
537
|
+
if option['name'] && option['name'] =~ /^#{Regexp.escape(s)}/
|
538
|
+
matches << option['name']
|
539
|
+
# elsif option['id'] && option['id'].to_s =~ /^#{Regexp.escape(s)}/
|
540
|
+
elsif option[value_field] && option[value_field].to_s == s
|
541
|
+
matches << option['name']
|
542
|
+
end
|
543
|
+
}
|
544
|
+
matches
|
545
|
+
}
|
546
|
+
# prompt for typeahead input value
|
547
|
+
input = Readline.readline("#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? ('(' + option_type['fieldAddOn'] + ') ') : '' }#{!option_type['required'] ? ' (optional)' : ''}#{!default_value.to_s.empty? ? ' ['+default_value.to_s+']' : ''} ['?' for options]: ", false).to_s
|
548
|
+
input = input.chomp.strip
|
549
|
+
end
|
550
|
+
|
551
|
+
# just hit enter, use [default] if set
|
552
|
+
if input.empty? && default_value
|
553
|
+
input = default_value.to_s
|
554
|
+
end
|
555
|
+
|
556
|
+
# not required and no value? ok proceed
|
557
|
+
if input.to_s == "" && option_type['required'] != true
|
558
|
+
value_found = true
|
559
|
+
value = nil # or "" # hmm
|
560
|
+
#next
|
561
|
+
break
|
562
|
+
end
|
563
|
+
|
564
|
+
# required and no value? you need help
|
565
|
+
# if input.to_s == "" && option_type['required'] == true
|
566
|
+
# help_prompt(option_type)
|
567
|
+
# display_select_options(option_type, select_options) unless select_options.empty?
|
568
|
+
# next
|
569
|
+
# end
|
570
|
+
|
571
|
+
# looking for help with this input
|
572
|
+
if input == '?'
|
573
|
+
help_prompt(option_type)
|
574
|
+
display_select_options(option_type, select_options) unless select_options.empty?
|
575
|
+
next
|
576
|
+
end
|
577
|
+
|
578
|
+
# just hit enter? scram
|
579
|
+
# looking for help with this input
|
580
|
+
# if input == ""
|
581
|
+
# help_prompt(option_type)
|
582
|
+
# display_select_options(option_type, select_options)
|
583
|
+
# next
|
584
|
+
# end
|
585
|
+
|
586
|
+
# this is how typeahead works, it keeps refreshing the options with a new ?query={value}
|
587
|
+
# query_value = (value || use_value || default_value || '')
|
588
|
+
query_value = (input || '')
|
589
|
+
api_params ||= {}
|
590
|
+
api_params['query'] = query_value
|
591
|
+
# skip refresh if you just hit enter
|
592
|
+
if !query_value.empty?
|
593
|
+
select_options = load_options(option_type, api_client, api_params, query_value)
|
594
|
+
end
|
595
|
+
|
596
|
+
# match input to option name or value
|
597
|
+
# actually that is redundant, it should already be filtered to matches
|
598
|
+
# and can just do this:
|
599
|
+
# select_option = select_options.size == 1 ? select_options[0] : nil
|
600
|
+
select_option = select_options.find{|b| (b[value_field] && (b[value_field].to_s == input.to_s)) || ((b[value_field].nil? || b[value_field].empty?) && (input == "")) }
|
601
|
+
if select_option.nil?
|
602
|
+
select_option = select_options.find{|b| b['name'] && b['name'] == input }
|
603
|
+
end
|
604
|
+
|
605
|
+
# found matching value, else did not find a value, show matching options and prompt again or error
|
606
|
+
if select_option
|
607
|
+
value = select_option[value_field]
|
608
|
+
set_last_select(select_option)
|
609
|
+
value_found = true
|
610
|
+
else
|
611
|
+
if use_value || no_prompt
|
612
|
+
# todo: make this nicer
|
613
|
+
# help_prompt(option_type)
|
614
|
+
print Term::ANSIColor.red, "\nMissing Required Option\n\n", Term::ANSIColor.reset
|
615
|
+
print Term::ANSIColor.red, " * #{option_type['fieldLabel']} [-O #{help_field_key}=] - #{option_type['description']}\n", Term::ANSIColor.reset
|
616
|
+
if select_options && select_options.size > 10
|
617
|
+
display_select_options(option_type, select_options.first(10))
|
618
|
+
puts " (#{select_options.size-10} more)"
|
619
|
+
else
|
620
|
+
display_select_options(option_type, select_options)
|
621
|
+
end
|
622
|
+
print "\n"
|
623
|
+
if select_options.empty?
|
624
|
+
print "The value '#{input}' matched 0 options.\n"
|
625
|
+
# print "Please try again.\n"
|
626
|
+
else
|
627
|
+
print "The value '#{input}' matched #{select_options.size()} options.\n"
|
628
|
+
print "Perhaps you meant one of these? #{ored_list(select_options.collect {|i|i['name']}, 3)}\n"
|
629
|
+
# print "Please try again.\n"
|
630
|
+
end
|
631
|
+
print "\n"
|
632
|
+
exit 1
|
633
|
+
else
|
634
|
+
#help_prompt(option_type)
|
635
|
+
display_select_options(option_type, select_options)
|
636
|
+
print "\n"
|
637
|
+
if select_options.empty?
|
638
|
+
print "The value '#{input}' matched 0 options.\n"
|
639
|
+
print "Please try again.\n"
|
640
|
+
else
|
641
|
+
print "The value '#{input}' matched #{select_options.size()} options.\n"
|
642
|
+
print "Perhaps you meant one of these? #{ored_list(select_options.collect {|i|i['name']}, 3)}\n"
|
643
|
+
print "Please try again.\n"
|
644
|
+
end
|
645
|
+
print "\n"
|
646
|
+
# reprompting now...
|
647
|
+
end
|
648
|
+
end
|
649
|
+
end # end while !value_found
|
650
|
+
|
651
|
+
# wrap in object when using fieldInput
|
652
|
+
if value && !option_type['fieldInput'].nil?
|
653
|
+
value = {option_type['fieldName'].split('.').last => value, option_type['fieldInput'] => (no_prompt ? option_type['defaultInputValue'] : field_input_prompt(option_type))}
|
654
|
+
end
|
655
|
+
value
|
656
|
+
end
|
657
|
+
|
461
658
|
# this is a funky one, the user is prompted for yes/no
|
462
659
|
# but the return value is 'on','off',nil
|
463
660
|
# todo: maybe make this easier to use, and have the api's be flexible too..
|
@@ -544,6 +741,9 @@ module Morpheus
|
|
544
741
|
# value = input.empty? ? option_type['defaultValue'] : input
|
545
742
|
if input == '?' && value.nil?
|
546
743
|
help_prompt(option_type)
|
744
|
+
elsif input.chomp == '' && value.nil?
|
745
|
+
# just hit enter right away to skip this
|
746
|
+
value_found = true
|
547
747
|
elsif input.chomp == 'EOF'
|
548
748
|
value_found = true
|
549
749
|
else
|
@@ -682,6 +882,42 @@ module Morpheus
|
|
682
882
|
return file_params
|
683
883
|
end
|
684
884
|
|
885
|
+
def self.load_options(option_type, api_client, api_params, query_value=nil)
|
886
|
+
select_options = []
|
887
|
+
# local array of options
|
888
|
+
if option_type['selectOptions']
|
889
|
+
# calculate from inline lambda
|
890
|
+
if option_type['selectOptions'].is_a?(Proc)
|
891
|
+
select_options = option_type['selectOptions'].call(api_client, grails_params(api_params || {}))
|
892
|
+
else
|
893
|
+
select_options = option_type['selectOptions']
|
894
|
+
end
|
895
|
+
# filter options ourselves
|
896
|
+
if query_value.to_s != ""
|
897
|
+
filtered_options = select_options.select { |it| it['value'].to_s == query_value.to_s }
|
898
|
+
if filtered_options.empty?
|
899
|
+
filtered_options = select_options.select { |it| it['name'].to_s == query_value.to_s }
|
900
|
+
end
|
901
|
+
select_options = filtered_options
|
902
|
+
end
|
903
|
+
elsif option_type['optionSource']
|
904
|
+
# calculate from inline lambda
|
905
|
+
if option_type['optionSource'].is_a?(Proc)
|
906
|
+
select_options = option_type['optionSource'].call(api_client, grails_params(api_params || {}))
|
907
|
+
elsif option_type['optionSource'] == 'list'
|
908
|
+
# /api/options/list is a special action for custom OptionTypeLists, just need to pass the optionTypeId parameter
|
909
|
+
select_options = load_source_options(option_type['optionSource'], api_client, grails_params(api_params || {}).merge({'optionTypeId' => option_type['id']}))
|
910
|
+
else
|
911
|
+
# remote optionSource aka /api/options/$optionSource?
|
912
|
+
select_options = load_source_options(option_type['optionSource'], api_client, grails_params(api_params || {}))
|
913
|
+
end
|
914
|
+
else
|
915
|
+
raise "option '#{field_key}' is type: 'typeahead' and missing selectOptions or optionSource!"
|
916
|
+
end
|
917
|
+
|
918
|
+
return select_options
|
919
|
+
end
|
920
|
+
|
685
921
|
def self.help_prompt(option_type)
|
686
922
|
field_key = [option_type['fieldContext'], option_type['fieldName']].select {|it| it && it != '' }.join('.')
|
687
923
|
help_field_key = option_type[:help_field_prefix] ? "#{option_type[:help_field_prefix]}.#{field_key}" : field_key
|
@@ -691,6 +927,11 @@ module Morpheus
|
|
691
927
|
else
|
692
928
|
print Term::ANSIColor.green," * #{option_type['fieldLabel']} [-O #{help_field_key}=] - ", Term::ANSIColor.reset , "#{option_type['description']}\n"
|
693
929
|
end
|
930
|
+
if option_type['type'].to_s == 'typeahead'
|
931
|
+
print "This is a typeahead input. Enter the name or value of an option.\n"
|
932
|
+
print "If the specified input matches more than one option, they will be printed and you will be prompted again.\n"
|
933
|
+
print "the matching options will be shown and you can try again.\n"
|
934
|
+
end
|
694
935
|
end
|
695
936
|
|
696
937
|
|
@@ -698,7 +939,8 @@ module Morpheus
|
|
698
939
|
api_client.options.options_for_source(source,params)['data']
|
699
940
|
end
|
700
941
|
|
701
|
-
def self.
|
942
|
+
def self.format_select_options_help(opt, select_options = [], paging = nil)
|
943
|
+
out = ""
|
702
944
|
header = opt['fieldLabel'] ? "#{opt['fieldLabel']} Options" : "Options"
|
703
945
|
if paging
|
704
946
|
offset = paging[:cur_page] * paging[:page_size]
|
@@ -706,11 +948,18 @@ module Morpheus
|
|
706
948
|
header = "#{header} (#{offset+1}-#{limit+1} of #{paging[:total]})"
|
707
949
|
select_options = select_options[(offset)..(limit)]
|
708
950
|
end
|
709
|
-
|
710
|
-
|
951
|
+
out = ""
|
952
|
+
out << "\n"
|
953
|
+
out << "#{header}\n"
|
954
|
+
out << "===============\n"
|
711
955
|
select_options.each do |option|
|
712
|
-
|
956
|
+
out << " * #{option['name']} [#{option['value']}]\n"
|
713
957
|
end
|
958
|
+
return out
|
959
|
+
end
|
960
|
+
|
961
|
+
def self.display_select_options(opt, select_options = [], paging = nil)
|
962
|
+
puts format_select_options_help(opt, select_options, paging)
|
714
963
|
end
|
715
964
|
|
716
965
|
def self.format_option_types_help(option_types, opts={})
|