morpheus-cli 5.0.0 → 5.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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={})
|