flipper-ui 0.2.0.beta1

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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +17 -0
  5. data/Guardfile +26 -0
  6. data/LICENSE +22 -0
  7. data/README.md +101 -0
  8. data/Rakefile +7 -0
  9. data/examples/basic.ru +44 -0
  10. data/examples/flipper.html +14 -0
  11. data/examples/flipper.png +0 -0
  12. data/flipper-ui.gemspec +21 -0
  13. data/lib/flipper-ui.rb +1 -0
  14. data/lib/flipper/ui.rb +23 -0
  15. data/lib/flipper/ui/action.rb +172 -0
  16. data/lib/flipper/ui/action_collection.rb +20 -0
  17. data/lib/flipper/ui/actions/features.rb +21 -0
  18. data/lib/flipper/ui/actions/file.rb +17 -0
  19. data/lib/flipper/ui/actions/gate.rb +143 -0
  20. data/lib/flipper/ui/actions/index.rb +17 -0
  21. data/lib/flipper/ui/assets/javascripts/application.coffee +305 -0
  22. data/lib/flipper/ui/assets/javascripts/spine/ajax.coffee +223 -0
  23. data/lib/flipper/ui/assets/javascripts/spine/list.coffee +43 -0
  24. data/lib/flipper/ui/assets/javascripts/spine/local.coffee +16 -0
  25. data/lib/flipper/ui/assets/javascripts/spine/manager.coffee +83 -0
  26. data/lib/flipper/ui/assets/javascripts/spine/relation.coffee +148 -0
  27. data/lib/flipper/ui/assets/javascripts/spine/route.coffee +146 -0
  28. data/lib/flipper/ui/assets/javascripts/spine/spine.coffee +542 -0
  29. data/lib/flipper/ui/assets/javascripts/spine/version +1 -0
  30. data/lib/flipper/ui/assets/stylesheets/application.scss +237 -0
  31. data/lib/flipper/ui/decorators/feature.rb +37 -0
  32. data/lib/flipper/ui/decorators/gate.rb +36 -0
  33. data/lib/flipper/ui/error.rb +10 -0
  34. data/lib/flipper/ui/eruby.rb +11 -0
  35. data/lib/flipper/ui/middleware.rb +66 -0
  36. data/lib/flipper/ui/public/css/application.css +183 -0
  37. data/lib/flipper/ui/public/css/images/animated-overlay.gif +0 -0
  38. data/lib/flipper/ui/public/css/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  39. data/lib/flipper/ui/public/css/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  40. data/lib/flipper/ui/public/css/images/ui-bg_flat_10_000000_40x100.png +0 -0
  41. data/lib/flipper/ui/public/css/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  42. data/lib/flipper/ui/public/css/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  43. data/lib/flipper/ui/public/css/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  44. data/lib/flipper/ui/public/css/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  45. data/lib/flipper/ui/public/css/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  46. data/lib/flipper/ui/public/css/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  47. data/lib/flipper/ui/public/css/images/ui-icons_222222_256x240.png +0 -0
  48. data/lib/flipper/ui/public/css/images/ui-icons_228ef1_256x240.png +0 -0
  49. data/lib/flipper/ui/public/css/images/ui-icons_ef8c08_256x240.png +0 -0
  50. data/lib/flipper/ui/public/css/images/ui-icons_ffd27a_256x240.png +0 -0
  51. data/lib/flipper/ui/public/css/images/ui-icons_ffffff_256x240.png +0 -0
  52. data/lib/flipper/ui/public/css/jquery-ui-1.10.3.slider.min.css +5 -0
  53. data/lib/flipper/ui/public/images/logo.png +0 -0
  54. data/lib/flipper/ui/public/images/remove.png +0 -0
  55. data/lib/flipper/ui/public/js/application.js +544 -0
  56. data/lib/flipper/ui/public/js/handlebars.js +1992 -0
  57. data/lib/flipper/ui/public/js/jquery-ui-1.10.3.slider.min.js +6 -0
  58. data/lib/flipper/ui/public/js/jquery.js +9555 -0
  59. data/lib/flipper/ui/public/js/jquery.min.js +4 -0
  60. data/lib/flipper/ui/public/js/jquery.min.map +1 -0
  61. data/lib/flipper/ui/public/js/spine/ajax.js +320 -0
  62. data/lib/flipper/ui/public/js/spine/list.js +72 -0
  63. data/lib/flipper/ui/public/js/spine/local.js +29 -0
  64. data/lib/flipper/ui/public/js/spine/manager.js +157 -0
  65. data/lib/flipper/ui/public/js/spine/relation.js +260 -0
  66. data/lib/flipper/ui/public/js/spine/route.js +223 -0
  67. data/lib/flipper/ui/public/js/spine/spine.js +927 -0
  68. data/lib/flipper/ui/util.rb +12 -0
  69. data/lib/flipper/ui/version.rb +5 -0
  70. data/lib/flipper/ui/views/index.erb +9 -0
  71. data/lib/flipper/ui/views/layout.erb +161 -0
  72. data/script/bootstrap +21 -0
  73. data/script/server +19 -0
  74. data/script/test +30 -0
  75. data/spec/flipper/ui/decorators/feature_spec.rb +59 -0
  76. data/spec/flipper/ui/decorators/gate_spec.rb +47 -0
  77. data/spec/flipper/ui/util_spec.rb +18 -0
  78. data/spec/flipper/ui_spec.rb +470 -0
  79. data/spec/helper.rb +35 -0
  80. metadata +168 -0
@@ -0,0 +1,20 @@
1
+ module Flipper
2
+ module UI
3
+ # Internal: Used to detect the action that should be used in the middleware.
4
+ class ActionCollection
5
+ def initialize
6
+ @action_classes = []
7
+ end
8
+
9
+ def add(action_class)
10
+ @action_classes << action_class
11
+ end
12
+
13
+ def action_for_request(request)
14
+ @action_classes.detect { |action_class|
15
+ request.path_info =~ action_class.regex
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ require 'flipper/ui/action'
2
+ require 'flipper/ui/decorators/feature'
3
+
4
+ module Flipper
5
+ module UI
6
+ module Actions
7
+ class Features < UI::Action
8
+
9
+ route %r{features/?\Z}
10
+
11
+ def get
12
+ features = flipper.features.map { |feature|
13
+ Decorators::Feature.new(feature)
14
+ }.sort_by(&:pretty_name)
15
+
16
+ json_response features.map(&:as_json)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ require 'rack/file'
2
+ require 'flipper/ui/action'
3
+
4
+ module Flipper
5
+ module UI
6
+ module Actions
7
+ class File < UI::Action
8
+
9
+ route %r{(images|css|js)/.*\Z}
10
+
11
+ def get
12
+ Rack::File.new(public_path).call(request.env)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,143 @@
1
+ require 'flipper/ui/util'
2
+ require 'flipper/ui/action'
3
+ require 'flipper/ui/actions/index'
4
+ require 'flipper/ui/decorators/feature'
5
+
6
+ module Flipper
7
+ module UI
8
+ module Actions
9
+ class Gate < UI::Action
10
+
11
+ # Private: Struct to wrap actors so they can respond to flipper_id.
12
+ FakeActor = Struct.new(:flipper_id)
13
+
14
+ route %r{features/.*/.*/?\Z}
15
+
16
+ # Get should run the index route. All the url does is control what is
17
+ # opened and closed when the page is loaded.
18
+ def get
19
+ run_other_action Index
20
+ end
21
+
22
+ # FIXME: Return more than just the gate as json response?
23
+ def post
24
+ feature_name, gate_name = request.path.split('/').pop(2).map{|value| Rack::Utils.unescape value }
25
+ update_gate_method_name = "update_#{gate_name}"
26
+
27
+ unless respond_to?(update_gate_method_name)
28
+ update_gate_method_undefined(gate_name)
29
+ end
30
+
31
+ feature = flipper[feature_name.to_sym]
32
+ send(update_gate_method_name, feature)
33
+ gate = feature.gate(gate_name)
34
+ value = feature.gate_values[gate.key]
35
+
36
+ json_response Decorators::Gate.new(gate, value).as_json
37
+ end
38
+
39
+ def update_boolean(feature)
40
+ if params['value'] == 'true'
41
+ feature.enable
42
+ else
43
+ feature.disable
44
+ end
45
+ end
46
+
47
+ def update_actor(feature)
48
+ value = params['value']
49
+
50
+ if Util.blank?(value)
51
+ invalid_actor_value(value)
52
+ end
53
+
54
+ thing = FakeActor.new(value)
55
+ actor = flipper.actor(thing)
56
+
57
+ case params['operation']
58
+ when 'enable'
59
+ feature.enable actor
60
+ when 'disable'
61
+ feature.disable actor
62
+ end
63
+ end
64
+
65
+ def update_group(feature)
66
+ group_name = params['value']
67
+ group = flipper.group(group_name)
68
+
69
+ case params['operation']
70
+ when 'enable'
71
+ feature.enable group
72
+ when 'disable'
73
+ feature.disable group
74
+ end
75
+ rescue Flipper::GroupNotRegistered => e
76
+ group_not_registered group_name
77
+ end
78
+
79
+ def update_percentage_of_actors(feature)
80
+ value = params['value']
81
+ feature.enable_percentage_of_actors value
82
+ rescue ArgumentError => exception
83
+ invalid_percentage value, exception
84
+ end
85
+
86
+ def update_percentage_of_random(feature)
87
+ value = params['value']
88
+ feature.enable_percentage_of_random value
89
+ rescue ArgumentError => exception
90
+ invalid_percentage value, exception
91
+ end
92
+
93
+ # Private: Returns error response for invalid actor value.
94
+ def invalid_actor_value(value)
95
+ response = {
96
+ status: 'error',
97
+ message: "#{value.inspect} is not a valid actor value.",
98
+ }
99
+
100
+ status 422
101
+ halt json_response(response)
102
+ end
103
+
104
+ # Private: Returns error response for invalid percentage value.
105
+ def invalid_percentage(value, exception)
106
+ response = {
107
+ status: 'error',
108
+ message: exception.message,
109
+ }
110
+
111
+ status 422
112
+ halt json_response(response)
113
+ end
114
+
115
+ # Private: Returns error response that group was not registered.
116
+ def group_not_registered(group_name)
117
+ response = {status: 'error'}
118
+
119
+ if Util.blank?(group_name)
120
+ status 422
121
+ response[:message] = "Group name is required."
122
+ else
123
+ status 404
124
+ response[:message] = "The group named #{group_name.inspect} has not been registered."
125
+ end
126
+
127
+ halt json_response(response)
128
+ end
129
+
130
+ # Private: Returns error response that gate update method is not defined.
131
+ def update_gate_method_undefined(gate_name)
132
+ response = {
133
+ status: 'error',
134
+ message: "I have no clue how to update the gate named #{gate_name.inspect}.",
135
+ }
136
+
137
+ status 404
138
+ halt json_response(response)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,17 @@
1
+ require 'flipper/ui/action'
2
+ require 'flipper/ui/decorators/feature'
3
+
4
+ module Flipper
5
+ module UI
6
+ module Actions
7
+ class Index < UI::Action
8
+
9
+ route %r{.*}
10
+
11
+ def get
12
+ view_response :index
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,305 @@
1
+ class Feature extends Spine.Model
2
+ @configure "Feature", "id", "name", "state", "description", "gates"
3
+ @extend Spine.Model.Ajax
4
+ @extend url: "#{Flipper.Config.url}/features"
5
+
6
+ constructor: ->
7
+ super
8
+ @gates = @gates.map (data) =>
9
+ data.feature_id = @id
10
+ new Gate(data)
11
+
12
+ gate: (name) ->
13
+ gates = @gates.filter (gate) ->
14
+ gate.name == name
15
+ gates[0]
16
+
17
+ window.Feature = Feature
18
+
19
+ class Gate extends Spine.Model
20
+ @configure "Gate", "feature_id", "key", "name", "value"
21
+
22
+ constructor: ->
23
+ super
24
+
25
+ url: ->
26
+ "#{Flipper.Config.url}/features/#{encodeURIComponent @feature_id}/#{encodeURIComponent @name}"
27
+
28
+ disableSetMember: (member, success_callback, error_callback) ->
29
+ @setMember('disable', member, success_callback, error_callback)
30
+
31
+ enableSetMember: (member, success_callback, error_callback) ->
32
+ @setMember('enable', member, success_callback, error_callback)
33
+
34
+ setMember: (operation, member, success_callback, error_callback) ->
35
+ options =
36
+ type: 'POST'
37
+ url: @url()
38
+ data:
39
+ operation: operation
40
+ value: member
41
+ success: (data, status, xhr) =>
42
+ Feature.trigger('reload')
43
+ @value = data.value
44
+ success_callback(data, status, xhr) if success_callback
45
+ error: (data, status, error) =>
46
+ response = if data.responseText then $.parseJSON data.responseText else message: "Something went wrong..."
47
+ alert "ERROR: #{response.message}"
48
+ error_callback(data, status) if error_callback
49
+
50
+ $.ajax options
51
+
52
+ save: (opts) ->
53
+ result = super
54
+ @ajaxSave(opts)
55
+ Feature.trigger('reload')
56
+ result
57
+
58
+ ajaxSave: (opts) ->
59
+ options =
60
+ type: 'POST'
61
+ url: @url()
62
+ data:
63
+ value: @value
64
+ error: (data, status, error) =>
65
+ response = if data.responseText then $.parseJSON data.responseText else message: "Something went wrong..."
66
+ alert "ERROR: #{response.message}"
67
+
68
+ $.ajax options
69
+
70
+ class App extends Spine.Controller
71
+ constructor: ->
72
+ super
73
+ @feature_list = new App.FeatureList(el: $('#features'))
74
+
75
+ class App.FeatureList extends Spine.Controller
76
+ constructor: ->
77
+ super
78
+ @feature_controllers = {}
79
+ Feature.bind "refresh", @addAll
80
+ Feature.bind "reload", @reload
81
+
82
+ Feature.one 'refresh', ->
83
+ Spine.Route.setup
84
+ history: true
85
+
86
+ Feature.fetch()
87
+
88
+ Spine.Route.add /features\/(.*)\/(.*)\/?/, (matches) =>
89
+ params =
90
+ id: matches.match[1]
91
+ gate: matches.match[2]
92
+ if controller = @feature_controllers[params.id]
93
+ controller.edit()
94
+ controller.activateGate(params)
95
+
96
+ Spine.Route.add /features\/(.*)\/?/, (matches) =>
97
+ params =
98
+ id: matches.match[1]
99
+ if controller = @feature_controllers[params.id]
100
+ controller.edit()
101
+ controller.openDefaultGate()
102
+
103
+ addOne: (feature) =>
104
+ controller = new App.Feature(feature: feature)
105
+ @feature_controllers[feature.id] = controller
106
+ @append controller.render()
107
+
108
+ addAll: =>
109
+ @html ''
110
+ $('#no_features').hide()
111
+ all_features = Feature.all()
112
+ if all_features.length > 0
113
+ @addOne feature for feature in all_features
114
+ else
115
+ $('#no_features').show()
116
+
117
+ reload: =>
118
+ Feature.fetch()
119
+ @addAll
120
+
121
+ class App.Feature extends Spine.Controller
122
+ elements:
123
+ '.feature': 'dom_feature'
124
+ '.gates': 'dom_gates'
125
+
126
+ events:
127
+ 'click .show-settings': 'openFeature'
128
+ 'click .hide-settings': 'hide'
129
+ 'click [data-tab]': 'clickTab'
130
+
131
+ constructor: ->
132
+ super
133
+ throw "@feature required" if !@feature?
134
+
135
+ render: ->
136
+ @html @template(@feature)
137
+ @gate_list = new App.GateList
138
+ el: @dom_gates
139
+ @el
140
+
141
+ openFeature: (event) ->
142
+ event.preventDefault() if event
143
+ @navigate "#{Flipper.Config.url}/features/#{@feature.id}"
144
+
145
+ openDefaultGate: ->
146
+ @navigate "#{Flipper.Config.url}/features/#{@feature.id}/boolean"
147
+
148
+ template: (feature) ->
149
+ source = $("#feature-template").html()
150
+ template = Handlebars.compile(source)
151
+ template(feature)
152
+
153
+ clickTab: (event) ->
154
+ event.preventDefault()
155
+ tab = $(event.currentTarget)
156
+ name = tab.attr('data-tab')
157
+ @navigate "#{Flipper.Config.url}/features/#{@feature.id}/#{name}"
158
+
159
+ activateGate: (params) ->
160
+ name = params.gate
161
+ @gate_list[name].active(params)
162
+ @el.find('[data-tab]').removeClass('active')
163
+ @el.find("[data-tab=#{name}]").addClass('active')
164
+
165
+ edit: (event) ->
166
+ event.preventDefault() if event
167
+ @dom_feature.addClass('settings')
168
+
169
+ hide: (event) ->
170
+ event.preventDefault() if event
171
+ @dom_feature.removeClass('settings')
172
+ @navigate "#{Flipper.Config.url}/"
173
+
174
+ class App.Gate extends Spine.Controller
175
+ constructor: ->
176
+ super
177
+ @active @renderForParams
178
+
179
+ renderForParams: (params) ->
180
+ @feature = Feature.find(params.id)
181
+ @gate = @feature.gate(params.gate)
182
+ @render()
183
+
184
+ render: ->
185
+ @html @template("#gate-#{@name.replace(/_/g, '-')}-template", @gate)
186
+ $slider = $(".slider-range")
187
+ $slider_value = $slider.siblings("input[type='text']")
188
+
189
+ $slider.slider
190
+ range: "min",
191
+ value: @gate.value,
192
+ min: 0,
193
+ max: 100,
194
+ slide: ( event, ui ) ->
195
+ $slider_value.val( ui.value )
196
+ return
197
+
198
+ $slider_value.val $slider.slider( "value" )
199
+ $slider_value.change ()->
200
+ $slider.slider "value", $(@).val()
201
+ return
202
+
203
+ template: (html_id, context) ->
204
+ source = $(html_id).html()
205
+ template = Handlebars.compile(source)
206
+ template(context)
207
+
208
+ class App.Gate.Boolean extends App.Gate
209
+ elements:
210
+ 'input[value=true]': 'input'
211
+
212
+ events:
213
+ 'submit form': 'submit'
214
+
215
+ constructor: ->
216
+ @name = 'boolean'
217
+ super
218
+
219
+ submit: (event) ->
220
+ event.preventDefault()
221
+ @gate.value = @input.is(':checked')
222
+ @gate.save()
223
+ @navigate "#{Flipper.Config.url}/"
224
+
225
+ class App.Gate.Set extends App.Gate
226
+ elements:
227
+ '.disable': 'dom_disable'
228
+ '.members': 'dom_members'
229
+ '[name=value]': 'dom_input'
230
+
231
+ events:
232
+ 'click .disable': 'disable'
233
+ 'submit form': 'submit'
234
+
235
+ disable: (event) ->
236
+ event.preventDefault()
237
+ member = $(event.currentTarget).closest('.member')
238
+ value = member.attr('data-value')
239
+ @gate.disableSetMember value, (data, status, xhr) ->
240
+ member.remove()
241
+
242
+ submit: (event) ->
243
+ event.preventDefault()
244
+ value = @dom_input.val()
245
+ self = @
246
+
247
+ @gate.enableSetMember value, (data, status, xhr) ->
248
+ html = self.template "#gate-member-template", value
249
+ self.dom_members.append html
250
+ self.dom_input.val ''
251
+
252
+ class App.Gate.Group extends App.Gate.Set
253
+ constructor: ->
254
+ @name = 'group'
255
+ super
256
+
257
+ class App.Gate.Actor extends App.Gate.Set
258
+ constructor: ->
259
+ @name = 'actor'
260
+ super
261
+
262
+ class App.Gate.Percentage extends App.Gate
263
+ elements:
264
+ 'input[type=text]': 'input'
265
+
266
+ events:
267
+ 'submit form': 'submit'
268
+
269
+ validate: ()->
270
+ float_value = parseFloat(@gate.value)
271
+ valid = true
272
+
273
+ if isNaN(float_value) || float_value < 0 || float_value > 100
274
+ alert "The percentage value provided is not valid"
275
+ valid = false
276
+
277
+ return valid
278
+
279
+ submit: (event) ->
280
+ event.preventDefault()
281
+ @gate.value = @input.val()
282
+ return unless @validate()
283
+ @gate.save()
284
+ @navigate "#{Flipper.Config.url}/"
285
+
286
+ class App.Gate.PercentageOfActors extends App.Gate.Percentage
287
+ constructor: ->
288
+ @name = 'percentage_of_actors'
289
+ super
290
+
291
+ class App.Gate.PercentageOfRandom extends App.Gate.Percentage
292
+ constructor: ->
293
+ @name = 'percentage_of_random'
294
+ super
295
+
296
+ class App.GateList extends Spine.Stack
297
+ controllers:
298
+ boolean: App.Gate.Boolean
299
+ group: App.Gate.Group
300
+ actor: App.Gate.Actor
301
+ percentage_of_actors: App.Gate.PercentageOfActors
302
+ percentage_of_random: App.Gate.PercentageOfRandom
303
+
304
+ jQuery ->
305
+ new App(el: $('#app'))