@@ -0,0 +1,69 @@ |
||
1 |
+$ -> |
|
2 |
+ $.fn.serializeObject = -> |
|
3 |
+ o = {} |
|
4 |
+ a = @serializeArray() |
|
5 |
+ $.each a, -> |
|
6 |
+ if o[@name] isnt `undefined` |
|
7 |
+ o[@name] = [o[@name]] unless o[@name].push |
|
8 |
+ o[@name].push @value or "" |
|
9 |
+ else |
|
10 |
+ o[@name] = @value or "" |
|
11 |
+ return |
|
12 |
+ o |
|
13 |
+ |
|
14 |
+ getFormData = (elem) -> |
|
15 |
+ form_data = $("#edit_agent, #new_agent").serializeObject() |
|
16 |
+ attribute = $(elem).data('attribute') |
|
17 |
+ form_data['attribute'] = attribute |
|
18 |
+ delete form_data['_method'] |
|
19 |
+ form_data |
|
20 |
+ |
|
21 |
+ window.initializeFormCompletable = -> |
|
22 |
+ returnedResults = {} |
|
23 |
+ completableDefaultOptions = (input) -> |
|
24 |
+ results: [ |
|
25 |
+ (returnedResults[$(input).data('attribute')] || {text: 'Options', children: [{id: '', text: 'loading ...'}]}) |
|
26 |
+ { |
|
27 |
+ text: 'Current', |
|
28 |
+ children: [id: $(input).val(), text: $(input).val()] |
|
29 |
+ }, |
|
30 |
+ { |
|
31 |
+ text: 'Custom', |
|
32 |
+ children: [id: 'manualInput', text: 'manual input'] |
|
33 |
+ }, |
|
34 |
+ ] |
|
35 |
+ |
|
36 |
+ $("input[role=validatable], select[role=validatable]").on 'change', (e) => |
|
37 |
+ form_data = getFormData(e.currentTarget) |
|
38 |
+ form_group = $(e.currentTarget).closest('.form-group') |
|
39 |
+ $.ajax '/agents/validate', |
|
40 |
+ type: 'POST', |
|
41 |
+ data: form_data |
|
42 |
+ success: (data) -> |
|
43 |
+ form_group.addClass('has-feedback').removeClass('has-error') |
|
44 |
+ form_group.find('span').addClass('hidden') |
|
45 |
+ form_group.find('.glyphicon-ok').removeClass('hidden') |
|
46 |
+ error: (data) -> |
|
47 |
+ form_group.addClass('has-feedback').addClass('has-error') |
|
48 |
+ form_group.find('span').addClass('hidden') |
|
49 |
+ form_group.find('.glyphicon-remove').removeClass('hidden') |
|
50 |
+ |
|
51 |
+ $("input[role=validatable], select[role=validatable]").trigger('change') |
|
52 |
+ |
|
53 |
+ $.each $("input[role~=completable]"), (i, input) -> |
|
54 |
+ $(input).select2 |
|
55 |
+ data: -> |
|
56 |
+ completableDefaultOptions(input) |
|
57 |
+ |
|
58 |
+ $("input[role~=completable]").on 'select2-open', (e) -> |
|
59 |
+ form_data = getFormData(e.currentTarget) |
|
60 |
+ return if returnedResults[form_data.attribute] |
|
61 |
+ |
|
62 |
+ $.ajax '/agents/complete', |
|
63 |
+ type: 'POST', |
|
64 |
+ data: form_data |
|
65 |
+ success: (data) -> |
|
66 |
+ console.log data |
|
67 |
+ returnedResults[form_data.attribute] = {text: 'Options', children: $.map(data, (d) -> {id: d.value, text: d.name})} |
|
68 |
+ $(e.currentTarget).trigger('change') |
|
69 |
+ $(e.currentTarget).select2('open') |
@@ -18,7 +18,6 @@ class @AgentEditPage |
||
18 | 18 |
else |
19 | 19 |
$(".agent-settings").show() |
20 | 20 |
$("#agent-spinner").fadeIn() |
21 |
- $("#agent_source_ids").select2("val", {}) |
|
22 | 21 |
$(".model-errors").hide() unless firstTime |
23 | 22 |
$.getJSON "/agents/type_details", { type: type }, (json) => |
24 | 23 |
if json.can_be_scheduled |
@@ -46,11 +45,12 @@ class @AgentEditPage |
||
46 | 45 |
|
47 | 46 |
$(".description").show().html(json.description_html) if json.description_html? |
48 | 47 |
|
49 |
- $('.oauthable-form').html(json.form) if json.form? |
|
50 |
- |
|
51 | 48 |
unless firstTime |
52 |
- window.jsonEditor.json = json.options |
|
53 |
- window.jsonEditor.rebuild() |
|
49 |
+ $('.oauthable-form').html(json.oauthable) if json.oauthable? |
|
50 |
+ $('.agent-options').html(json.form_options) if json.form_options? |
|
51 |
+ window.jsonEditor = setupJsonEditor()[0] |
|
52 |
+ |
|
53 |
+ window.initializeFormCompletable() |
|
54 | 54 |
|
55 | 55 |
$("#agent-spinner").stop(true, true).fadeOut(); |
56 | 56 |
|
@@ -0,0 +1,66 @@ |
||
1 |
+module FormConfigurable |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ included do |
|
5 |
+ class_attribute :_form_configurable_fields |
|
6 |
+ self._form_configurable_fields = HashWithIndifferentAccess.new { |h,k| h[k] = [] } |
|
7 |
+ end |
|
8 |
+ |
|
9 |
+ delegate :form_configurable_attributes, to: :class |
|
10 |
+ delegate :form_configurable_fields, to: :class |
|
11 |
+ |
|
12 |
+ def is_form_configurable? |
|
13 |
+ true |
|
14 |
+ end |
|
15 |
+ |
|
16 |
+ def validate_option(method) |
|
17 |
+ if self.respond_to? "validate_#{method}".to_sym |
|
18 |
+ self.send("validate_#{method}".to_sym) |
|
19 |
+ else |
|
20 |
+ false |
|
21 |
+ end |
|
22 |
+ end |
|
23 |
+ |
|
24 |
+ def complete_option(method) |
|
25 |
+ if self.respond_to? "complete_#{method}".to_sym |
|
26 |
+ self.send("complete_#{method}".to_sym) |
|
27 |
+ end |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+ module ClassMethods |
|
31 |
+ def form_configurable(name, *args) |
|
32 |
+ options = args.extract_options!.reverse_merge(roles: [], type: :string) |
|
33 |
+ |
|
34 |
+ if args.all? { |arg| arg.is_a?(Symbol) } |
|
35 |
+ options.assert_valid_keys([:type, :roles, :values]) |
|
36 |
+ end |
|
37 |
+ |
|
38 |
+ if options[:type] == :array && (options[:values].blank? || !options[:values].is_a?(Array)) |
|
39 |
+ raise ArgumentError.new('When using :array as :type you need to provide the :values as an Array') |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ if options[:roles].is_a?(Symbol) |
|
43 |
+ options[:roles] = [options[:roles]] |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ if options[:roles].include?(:completable) && !self.method_defined?("complete_#{name}".to_sym) |
|
47 |
+ # Not really sure, but method_defined? does not seem to work because we do not have the 'full' Agent class here |
|
48 |
+ #raise ArgumentError.new("'complete_#{name}' needs to be defined to validate '#{name}'") |
|
49 |
+ end |
|
50 |
+ |
|
51 |
+ if options[:roles].include?(:validatable) && !self.method_defined?("validate_#{name}".to_sym) |
|
52 |
+ #raise ArgumentError.new("'validate_#{name}' needs to be defined to validate '#{name}'") |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ _form_configurable_fields[name] = options |
|
56 |
+ end |
|
57 |
+ |
|
58 |
+ def form_configurable_fields |
|
59 |
+ self._form_configurable_fields |
|
60 |
+ end |
|
61 |
+ |
|
62 |
+ def form_configurable_attributes |
|
63 |
+ form_configurable_fields.keys |
|
64 |
+ end |
|
65 |
+ end |
|
66 |
+end |
@@ -35,6 +35,8 @@ class AgentsController < ApplicationController |
||
35 | 35 |
|
36 | 36 |
def type_details |
37 | 37 |
@agent = Agent.build_for_type(params[:type], current_user, {}) |
38 |
+ initialize_presenter |
|
39 |
+ |
|
38 | 40 |
render :json => { |
39 | 41 |
:can_be_scheduled => @agent.can_be_scheduled?, |
40 | 42 |
:default_schedule => @agent.default_schedule, |
@@ -43,7 +45,8 @@ class AgentsController < ApplicationController |
||
43 | 45 |
:can_control_other_agents => @agent.can_control_other_agents?, |
44 | 46 |
:options => @agent.default_options, |
45 | 47 |
:description_html => @agent.html_description, |
46 |
- :form => render_to_string(partial: 'oauth_dropdown', locals: { agent: @agent }) |
|
48 |
+ :oauthable => render_to_string(partial: 'oauth_dropdown', locals: { agent: @agent }), |
|
49 |
+ :form_options => render_to_string(partial: 'options', locals: { agent: @agent }) |
|
47 | 50 |
} |
48 | 51 |
end |
49 | 52 |
|
@@ -92,6 +95,7 @@ class AgentsController < ApplicationController |
||
92 | 95 |
else |
93 | 96 |
@agent = agents.build |
94 | 97 |
end |
98 |
+ initialize_presenter |
|
95 | 99 |
|
96 | 100 |
respond_to do |format| |
97 | 101 |
format.html |
@@ -101,17 +105,18 @@ class AgentsController < ApplicationController |
||
101 | 105 |
|
102 | 106 |
def edit |
103 | 107 |
@agent = current_user.agents.find(params[:id]) |
108 |
+ initialize_presenter |
|
104 | 109 |
end |
105 | 110 |
|
106 | 111 |
def create |
107 |
- @agent = Agent.build_for_type(params[:agent].delete(:type), |
|
108 |
- current_user, |
|
109 |
- params[:agent]) |
|
112 |
+ build_agent |
|
113 |
+ |
|
110 | 114 |
respond_to do |format| |
111 | 115 |
if @agent.save |
112 | 116 |
format.html { redirect_back "'#{@agent.name}' was successfully created." } |
113 | 117 |
format.json { render json: @agent, status: :ok, location: agent_path(@agent) } |
114 | 118 |
else |
119 |
+ initialize_presenter |
|
115 | 120 |
format.html { render action: "new" } |
116 | 121 |
format.json { render json: @agent.errors, status: :unprocessable_entity } |
117 | 122 |
end |
@@ -126,6 +131,7 @@ class AgentsController < ApplicationController |
||
126 | 131 |
format.html { redirect_back "'#{@agent.name}' was successfully updated." } |
127 | 132 |
format.json { render json: @agent, status: :ok, location: agent_path(@agent) } |
128 | 133 |
else |
134 |
+ initialize_presenter |
|
129 | 135 |
format.html { render action: "edit" } |
130 | 136 |
format.json { render json: @agent.errors, status: :unprocessable_entity } |
131 | 137 |
end |
@@ -153,6 +159,22 @@ class AgentsController < ApplicationController |
||
153 | 159 |
end |
154 | 160 |
end |
155 | 161 |
|
162 |
+ def validate |
|
163 |
+ build_agent |
|
164 |
+ |
|
165 |
+ if @agent.validate_option(params[:attribute]) |
|
166 |
+ render text: 'ok' |
|
167 |
+ else |
|
168 |
+ render text: 'error', status: 403 |
|
169 |
+ end |
|
170 |
+ end |
|
171 |
+ |
|
172 |
+ def complete |
|
173 |
+ build_agent |
|
174 |
+ |
|
175 |
+ render json: @agent.complete_option(params[:attribute]) |
|
176 |
+ end |
|
177 |
+ |
|
156 | 178 |
protected |
157 | 179 |
|
158 | 180 |
# Sanitize params[:return] to prevent open redirect attacks, a common security issue. |
@@ -167,4 +189,16 @@ class AgentsController < ApplicationController |
||
167 | 189 |
|
168 | 190 |
redirect_to path, notice: message |
169 | 191 |
end |
192 |
+ |
|
193 |
+ def build_agent |
|
194 |
+ @agent = Agent.build_for_type(params[:agent].delete(:type), |
|
195 |
+ current_user, |
|
196 |
+ params[:agent]) |
|
197 |
+ end |
|
198 |
+ |
|
199 |
+ def initialize_presenter |
|
200 |
+ if @agent.present? && @agent.is_form_configurable? |
|
201 |
+ @agent = FormConfigurableAgentPresenter.new(@agent, view_context) |
|
202 |
+ end |
|
203 |
+ end |
|
170 | 204 |
end |
@@ -88,6 +88,10 @@ class Agent < ActiveRecord::Base |
||
88 | 88 |
# Implement me in your subclass of Agent. |
89 | 89 |
end |
90 | 90 |
|
91 |
+ def is_form_configurable? |
|
92 |
+ false |
|
93 |
+ end |
|
94 |
+ |
|
91 | 95 |
def receive_web_request(params, method, format) |
92 | 96 |
# Implement me in your subclass of Agent. |
93 | 97 |
["not implemented", 404] |
@@ -1,5 +1,7 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class BasecampAgent < Agent |
3 |
+ include FormConfigurable |
|
4 |
+ |
|
3 | 5 |
cannot_receive_events! |
4 | 6 |
|
5 | 7 |
include Oauthable |
@@ -1,5 +1,7 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class HipchatAgent < Agent |
3 |
+ include FormConfigurable |
|
4 |
+ |
|
3 | 5 |
cannot_be_scheduled! |
4 | 6 |
cannot_create_events! |
5 | 7 |
|
@@ -33,6 +35,24 @@ module Agents |
||
33 | 35 |
} |
34 | 36 |
end |
35 | 37 |
|
38 |
+ form_configurable :auth_token, roles: :validatable |
|
39 |
+ form_configurable :room_name, roles: :completable |
|
40 |
+ form_configurable :username |
|
41 |
+ form_configurable :message, type: :text |
|
42 |
+ form_configurable :notify, type: :boolean |
|
43 |
+ form_configurable :color, type: :array, values: ['yellow', 'red', 'green', 'purple', 'gray', 'random'] |
|
44 |
+ |
|
45 |
+ def validate_auth_token |
|
46 |
+ client.rooms |
|
47 |
+ true |
|
48 |
+ rescue HipChat::UnknownResponseCode |
|
49 |
+ return false |
|
50 |
+ end |
|
51 |
+ |
|
52 |
+ def complete_room_name |
|
53 |
+ client.rooms.collect { |room| {name: room.name, value: room.name} } |
|
54 |
+ end |
|
55 |
+ |
|
36 | 56 |
def validate_options |
37 | 57 |
errors.add(:base, "you need to specify a hipchat auth_token or provide a credential named hipchat_auth_token") unless options['auth_token'].present? || credential('hipchat_auth_token').present? |
38 | 58 |
errors.add(:base, "you need to specify a room_name or a room_name_path") if options['room_name'].blank? && options['room_name_path'].blank? |
@@ -49,6 +69,7 @@ module Agents |
||
49 | 69 |
end |
50 | 70 |
end |
51 | 71 |
|
72 |
+ private |
|
52 | 73 |
def client |
53 | 74 |
@client ||= HipChat::Client.new(interpolated[:auth_token] || credential('hipchat_auth_token')) |
54 | 75 |
end |
@@ -0,0 +1,41 @@ |
||
1 |
+require 'delegate' |
|
2 |
+ |
|
3 |
+class Decorator < SimpleDelegator |
|
4 |
+ def class |
|
5 |
+ __getobj__.class |
|
6 |
+ end |
|
7 |
+end |
|
8 |
+ |
|
9 |
+class FormConfigurableAgentPresenter < Decorator |
|
10 |
+ def initialize(agent, view) |
|
11 |
+ @agent = agent |
|
12 |
+ @view = view |
|
13 |
+ super(agent) |
|
14 |
+ end |
|
15 |
+ |
|
16 |
+ def option_field_for(attribute) |
|
17 |
+ data = @agent.form_configurable_fields[attribute] |
|
18 |
+ value = @agent.options[attribute.to_s] || @agent.default_options[attribute.to_s] |
|
19 |
+ html_options = {role: data[:roles].join(' '), data: {attribute: attribute}} |
|
20 |
+ |
|
21 |
+ case data[:type] |
|
22 |
+ when :text |
|
23 |
+ @view.text_area_tag "agent[options][#{attribute}]", value, html_options.merge(class: 'form-control', rows: 3) |
|
24 |
+ when :boolean |
|
25 |
+ @view.content_tag 'div' do |
|
26 |
+ @view.concat(@view.content_tag('label', class: 'radio-inline') do |
|
27 |
+ @view.concat @view.radio_button_tag "agent[options][#{attribute}]", 'true', @agent.send(:boolify, value), html_options |
|
28 |
+ @view.concat "Yes" |
|
29 |
+ end) |
|
30 |
+ @view.concat(@view.content_tag('label', class: 'radio-inline') do |
|
31 |
+ @view.concat @view.radio_button_tag "agent[options][#{attribute}]", 'false', !@agent.send(:boolify, value), html_options |
|
32 |
+ @view.concat "No" |
|
33 |
+ end) |
|
34 |
+ end |
|
35 |
+ when :array |
|
36 |
+ @view.select_tag "agent[options][#{attribute}]", @view.options_for_select(data[:values], value), html_options.merge(class: "form-control") |
|
37 |
+ when :string |
|
38 |
+ @view.text_field_tag "agent[options][#{attribute}]", value, html_options.merge(:class => 'form-control') |
|
39 |
+ end |
|
40 |
+ end |
|
41 |
+end |
@@ -16,8 +16,7 @@ |
||
16 | 16 |
<div class="col-md-6"> |
17 | 17 |
<div class="row"> |
18 | 18 |
|
19 |
- <!-- Form controls width restricted --> |
|
20 |
- <div class="col-md-8"> |
|
19 |
+ <div class="col-md-12"> |
|
21 | 20 |
<% if @agent.new_record? %> |
22 | 21 |
<div class="form-group type-select"> |
23 | 22 |
<%= f.label :type %> |
@@ -27,7 +26,7 @@ |
||
27 | 26 |
</div> |
28 | 27 |
|
29 | 28 |
<div class="agent-settings"> |
30 |
- <div class="col-md-8"> |
|
29 |
+ <div class="col-md-12"> |
|
31 | 30 |
<div class="form-group"> |
32 | 31 |
<%= f.label :name %> |
33 | 32 |
<%= f.text_field :name, :class => 'form-control' %> |
@@ -105,19 +104,8 @@ |
||
105 | 104 |
|
106 | 105 |
</div> |
107 | 106 |
|
108 |
- <!-- Form controls full width --> |
|
109 |
- <div class="col-md-12"> |
|
110 |
- <div class="form-group"> |
|
111 |
- <%= f.label :options %> |
|
112 |
- <span class="glyphicon glyphicon-question-sign hover-help" data-content="In this JSON hash, interpolation is available in almost all values using the Liquid templating language.<p>Available template variables include the following:<dl><dt><code>message</code>, <code>url</code>, etc.</dt><dd>Refers to the corresponding key's value of each incoming event's payload.</dd><dt><code>agent</code></dt><dd>Refers to the agent that created each incoming event. It has attributes like <code>type</code>, <code>name</code> and <code>options</code>, so <code>{{agent.type}}</code> will expand to <code>WebsiteAgent</code> if an incoming event is created by that agent.</dd></dl></p><p>To access user credentials, use the <code>credential</code> tag like this: <code>{% credential <em>bare_key_name</em> %}</code></p>"></span> |
|
113 |
- <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor"> |
|
114 |
- <%= Utils.jsonify((@agent.new_record? && @agent.options == {}) ? @agent.default_options : @agent.options) %> |
|
115 |
- </textarea> |
|
116 |
- </div> |
|
117 |
- |
|
118 |
- <div class="form-group"> |
|
119 |
- <%= f.submit "Save", :class => "btn btn-primary" %> |
|
120 |
- </div> |
|
107 |
+ <div class="col-md-12 agent-options"> |
|
108 |
+ <%= render partial: 'options', locals: { agent: @agent } %> |
|
121 | 109 |
</div> |
122 | 110 |
</div> |
123 | 111 |
</div> |
@@ -0,0 +1,27 @@ |
||
1 |
+<% if agent.is_form_configurable? %> |
|
2 |
+ <fieldset> |
|
3 |
+ <% if agent.persisted? %> |
|
4 |
+ <%= hidden_field_tag 'agent[type]', @agent.type %> |
|
5 |
+ <% end %> |
|
6 |
+ <legend>Options</legend> |
|
7 |
+ <% agent.form_configurable_attributes.each do |attribute| %> |
|
8 |
+ <div class="form-group"> |
|
9 |
+ <%= label_tag attribute %> |
|
10 |
+ <%= agent.option_field_for(attribute) %> |
|
11 |
+ <span class="glyphicon glyphicon-ok form-control-feedback hidden"></span> |
|
12 |
+ <span class="glyphicon glyphicon-remove form-control-feedback hidden"></span> |
|
13 |
+ </div> |
|
14 |
+ <% end %> |
|
15 |
+ </fieldset> |
|
16 |
+<% else %> |
|
17 |
+ <div class="form-group"> |
|
18 |
+ <%= label_tag :options %> |
|
19 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="In this JSON hash, interpolation is available in almost all values using the Liquid templating language.<p>Available template variables include the following:<dl><dt><code>message</code>, <code>url</code>, etc.</dt><dd>Refers to the corresponding key's value of each incoming event's payload.</dd><dt><code>agent</code></dt><dd>Refers to the agent that created each incoming event. It has attributes like <code>type</code>, <code>name</code> and <code>options</code>, so <code>{{agent.type}}</code> will expand to <code>WebsiteAgent</code> if an incoming event is created by that agent.</dd></dl></p><p>To access user credentials, use the <code>credential</code> tag like this: <code>{% credential <em>bare_key_name</em> %}</code></p>"></span> |
|
20 |
+ <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor"> |
|
21 |
+ <%= Utils.jsonify((agent.new_record? && agent.options == {}) ? agent.default_options : agent.options) %> |
|
22 |
+ </textarea> |
|
23 |
+ </div> |
|
24 |
+<% end %> |
|
25 |
+<div class="form-group"> |
|
26 |
+ <%= submit_tag "Save", :class => "btn btn-primary" %> |
|
27 |
+</div> |
@@ -9,8 +9,9 @@ |
||
9 | 9 |
</div> |
10 | 10 |
</div> |
11 | 11 |
</div> |
12 |
- |
|
13 |
- <%= render 'form' %> |
|
12 |
+ <div id="agent-form"> |
|
13 |
+ <%= render 'form' %> |
|
14 |
+ </div> |
|
14 | 15 |
|
15 | 16 |
<hr> |
16 | 17 |
|
@@ -8,7 +8,9 @@ |
||
8 | 8 |
</h2> |
9 | 9 |
</div> |
10 | 10 |
|
11 |
- <%= render 'form' %> |
|
11 |
+ <div id="agent-form"> |
|
12 |
+ <%= render 'form' %> |
|
13 |
+ </div> |
|
12 | 14 |
|
13 | 15 |
<hr> |
14 | 16 |
|
@@ -13,7 +13,7 @@ module Huginn |
||
13 | 13 |
# -- all .rb files in that directory are automatically loaded. |
14 | 14 |
|
15 | 15 |
# Custom directories with classes and modules you want to be autoloadable. |
16 |
- config.autoload_paths += %W(#{config.root}/lib) |
|
16 |
+ config.autoload_paths += %W(#{config.root}/lib #{config.root}/app/presenters) |
|
17 | 17 |
|
18 | 18 |
# Activate observers that should always be running. |
19 | 19 |
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer |
@@ -11,6 +11,8 @@ Huginn::Application.routes.draw do |
||
11 | 11 |
post :propagate |
12 | 12 |
get :type_details |
13 | 13 |
get :event_descriptions |
14 |
+ post :validate |
|
15 |
+ post :complete |
|
14 | 16 |
end |
15 | 17 |
|
16 | 18 |
resources :logs, :only => [:index] do |