flipper-ui 0.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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'))