vines-services 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/LICENSE +19 -0
  2. data/README +40 -0
  3. data/Rakefile +130 -0
  4. data/bin/vines-services +95 -0
  5. data/conf/config.rb +25 -0
  6. data/lib/vines/services/command/init.rb +209 -0
  7. data/lib/vines/services/command/restart.rb +14 -0
  8. data/lib/vines/services/command/start.rb +30 -0
  9. data/lib/vines/services/command/stop.rb +20 -0
  10. data/lib/vines/services/command/views.rb +26 -0
  11. data/lib/vines/services/component.rb +26 -0
  12. data/lib/vines/services/config.rb +105 -0
  13. data/lib/vines/services/connection.rb +120 -0
  14. data/lib/vines/services/controller/attributes_controller.rb +19 -0
  15. data/lib/vines/services/controller/base_controller.rb +99 -0
  16. data/lib/vines/services/controller/disco_info_controller.rb +61 -0
  17. data/lib/vines/services/controller/labels_controller.rb +17 -0
  18. data/lib/vines/services/controller/members_controller.rb +44 -0
  19. data/lib/vines/services/controller/messages_controller.rb +66 -0
  20. data/lib/vines/services/controller/probes_controller.rb +45 -0
  21. data/lib/vines/services/controller/services_controller.rb +81 -0
  22. data/lib/vines/services/controller/subscriptions_controller.rb +39 -0
  23. data/lib/vines/services/controller/systems_controller.rb +45 -0
  24. data/lib/vines/services/controller/transfers_controller.rb +58 -0
  25. data/lib/vines/services/controller/uploads_controller.rb +62 -0
  26. data/lib/vines/services/controller/users_controller.rb +127 -0
  27. data/lib/vines/services/core_ext/blather.rb +46 -0
  28. data/lib/vines/services/core_ext/couchrest.rb +33 -0
  29. data/lib/vines/services/indexer.rb +195 -0
  30. data/lib/vines/services/priority_queue.rb +94 -0
  31. data/lib/vines/services/roster.rb +70 -0
  32. data/lib/vines/services/storage/couchdb/fragment.rb +23 -0
  33. data/lib/vines/services/storage/couchdb/service.rb +170 -0
  34. data/lib/vines/services/storage/couchdb/system.rb +141 -0
  35. data/lib/vines/services/storage/couchdb/upload.rb +66 -0
  36. data/lib/vines/services/storage/couchdb/user.rb +137 -0
  37. data/lib/vines/services/storage/couchdb/vcard.rb +13 -0
  38. data/lib/vines/services/storage/couchdb.rb +157 -0
  39. data/lib/vines/services/storage.rb +33 -0
  40. data/lib/vines/services/throttle.rb +26 -0
  41. data/lib/vines/services/version.rb +7 -0
  42. data/lib/vines/services/vql/compiler.rb +94 -0
  43. data/lib/vines/services/vql/vql.citrus +115 -0
  44. data/lib/vines/services/vql/vql.rb +186 -0
  45. data/lib/vines/services.rb +71 -0
  46. data/test/config_test.rb +242 -0
  47. data/test/priority_queue_test.rb +23 -0
  48. data/test/storage/couchdb_test.rb +30 -0
  49. data/test/vql/compiler_test.rb +96 -0
  50. data/test/vql/vql_test.rb +233 -0
  51. data/web/coffeescripts/api.coffee +51 -0
  52. data/web/coffeescripts/commands.coffee +18 -0
  53. data/web/coffeescripts/files.coffee +315 -0
  54. data/web/coffeescripts/init.coffee +21 -0
  55. data/web/coffeescripts/services.coffee +356 -0
  56. data/web/coffeescripts/setup.coffee +503 -0
  57. data/web/coffeescripts/systems.coffee +371 -0
  58. data/web/images/default-service.png +0 -0
  59. data/web/images/linux.png +0 -0
  60. data/web/images/mac.png +0 -0
  61. data/web/images/run.png +0 -0
  62. data/web/images/windows.png +0 -0
  63. data/web/index.html +17 -0
  64. data/web/stylesheets/common.css +52 -0
  65. data/web/stylesheets/files.css +218 -0
  66. data/web/stylesheets/services.css +181 -0
  67. data/web/stylesheets/setup.css +117 -0
  68. data/web/stylesheets/systems.css +142 -0
  69. metadata +230 -0
@@ -0,0 +1,315 @@
1
+ class FilesPage
2
+ FILES = 'http://getvines.com/protocol/files'
3
+ LABELS = 'http://getvines.com/protocol/files/labels'
4
+
5
+ constructor: (@session) ->
6
+ @api = new Api @session
7
+ @uploads = new Uploads
8
+ session: @session
9
+ jid: @api.jid
10
+ size: this.size
11
+ complete: (file) =>
12
+ this.fileNode(file)
13
+ this.findFiles name: file.name
14
+
15
+ findLabels: ->
16
+ $('#labels').empty()
17
+ @api.get LABELS, {}, (result) =>
18
+ this.labelNodeList row for row in result.rows
19
+
20
+ labelNodeList: (label)->
21
+ text = if label.size == 1 then 'file' else 'files'
22
+ node = $("""
23
+ <li data-name="" style='display:none;'>
24
+ <span class="text"></span>
25
+ <span class="count">#{label.size} #{text}</span>
26
+ </li>
27
+ """).appendTo '#labels'
28
+ $('.text', node).text label.name
29
+ node.attr 'data-name', label.name
30
+ node.click (event) => this.selectLabel(event)
31
+ node.fadeIn(100)
32
+
33
+ selectLabel: (event) ->
34
+ name = $(event.currentTarget).attr 'data-name'
35
+ $('#labels li').removeClass 'selected'
36
+ $(event.currentTarget).addClass 'selected'
37
+ $('#files').empty()
38
+ this.findFiles label: $(event.currentTarget).attr('data-name')
39
+
40
+ findFiles: (criteria) ->
41
+ @api.get FILES, criteria, (result) =>
42
+ this.fileNode row for row in result.rows
43
+
44
+ fileNode: (file) ->
45
+ size = this.size file.size
46
+ if !file.created_at
47
+ file.created_at = Date()
48
+ time = this.date file.created_at
49
+ node = $("""
50
+ <li data-id="#{file.id}" data-name="" data-size="#{size}" data-created="#{time}">
51
+ <div class="file-icon">
52
+ <span class="size">#{size}</span>
53
+ </div>
54
+ <h2></h2>
55
+ <footer>
56
+ <span class="time">#{time}</span>
57
+ <ul class="labels"></ul>
58
+ <form class="add-label">
59
+ <div class="add-label-button"></div>
60
+ <input type="text" placeholder="Label" style="display:none;"/>
61
+ </form>
62
+ </footer>
63
+ <form class="file-form">
64
+ <fieldset>
65
+ <input class="cancel" type="submit" value="Delete"/>
66
+ </fieldset>
67
+ </form>
68
+ </li>
69
+ """).appendTo '#files'
70
+
71
+ node.data 'file', file
72
+ $('h2', node).text file.name
73
+ node.attr 'data-name', file.name
74
+
75
+ new Button $('.file-icon', node).get(0), ICONS.page2,
76
+ scale: 1.0
77
+ translation: '-2 0'
78
+ 'stroke-width': 0.1
79
+ opacity: 1.0
80
+
81
+ new Button $('.add-label-button', node).get(0), ICONS.plus,
82
+ translation: '-10 -10'
83
+ scale: 0.5
84
+
85
+ $('form.file-form', node).submit => this.deleteFile node
86
+ $('form.add-label', node).submit => this.addLabel node
87
+ $('.add-label-button', node).click ->
88
+ $('form.add-label input[type="text"]', node).show()
89
+
90
+ this.labelNode node, label for label in file.labels
91
+
92
+ labelNode: (node, label) ->
93
+ labels = $('.labels', node)
94
+ item = $("""
95
+ <li data-name="">
96
+ <span class="text"></span>
97
+ <div class="remove"></div>
98
+ </li>
99
+ """).appendTo labels
100
+ $('.text', item).text label
101
+ item.attr 'data-name', label
102
+
103
+ new Button $('.remove', item).get(0), ICONS.cross,
104
+ translation: '-8 -8'
105
+ scale: 0.5
106
+
107
+ $('.remove', item).click =>
108
+ this.removeLabel node, item
109
+
110
+ addLabel: (node) ->
111
+ input = $('form.add-label input[type="text"]', node)
112
+ input.hide()
113
+ labels = (val for val in input.val().split(/,/) when val)
114
+ input.val ''
115
+ file = node.data 'file'
116
+ file.labels.push label for label in labels
117
+ @api.save FILES, file, (result) ->
118
+ this.labelNode node, label for label in labels
119
+ this.findLabels()
120
+ false
121
+
122
+ removeLabel: (node, item) ->
123
+ file = node.data 'file'
124
+ remove = item.attr 'data-name'
125
+ file.labels = (label for label in file.labels when label != remove)
126
+ @api.save FILES, file, (result) ->
127
+ item.fadeOut 200, -> item.remove()
128
+ this.findLabels()
129
+
130
+ deleteFile: (node) ->
131
+ @api.remove FILES, node.attr('data-id'), (result) =>
132
+ node.fadeOut 200, -> node.remove()
133
+ false
134
+
135
+ date: (date) ->
136
+ date = new Date date
137
+ day = 'Sun Mon Tue Wed Thu Fri Sat'.split(' ')[date.getDay()]
138
+ month = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ')[date.getMonth()]
139
+ "#{day}, #{date.getDate()} #{month}, #{date.getFullYear()} @ #{date.getHours()}:#{date.getMinutes()}"
140
+
141
+ size: (bytes) ->
142
+ kb = bytes / 1024
143
+ mb = kb / 1024
144
+ gb = mb / 1024
145
+ fmt = (num) ->
146
+ if num >= 100
147
+ Math.round num
148
+ else
149
+ num.toFixed(1).replace '.0', ''
150
+
151
+ if kb < 1
152
+ "#{bytes} b"
153
+ else if mb < 1
154
+ "#{fmt kb} k"
155
+ else if gb < 1
156
+ "#{fmt mb} m"
157
+ else
158
+ "#{fmt gb} g"
159
+
160
+ draw: ->
161
+ unless @session.connected()
162
+ window.location.hash = ''
163
+ return
164
+
165
+ $('body').attr 'id', 'files-page'
166
+ $('#container').hide().empty()
167
+ $("""
168
+ <div id="alpha" class="sidebar column y-fill">
169
+ <h2>Labels</h2>
170
+ <ul id="labels" class="selectable scroll y-fill"></ul>
171
+ <div id="alpha-controls" class="controls"></div>
172
+ </div>
173
+ <div id="beta" class="primary column x-fill y-fill">
174
+ <h2 id="files-title">Files <div id="search-files-icon"></div></h2>
175
+ <div id="search-files-form"></div>
176
+ <ul id="files" class="scroll y-fill"></ul>
177
+ </div>
178
+ <div id="charlie" class="sidebar column y-fill">
179
+ <h2>Uploads</h2>
180
+ <div id="upload-dnd" class="float">Drag files here to upload.</div>
181
+ <ul id="uploads" class="scroll y-fill"></ul>
182
+ <div id="charlie-controls" class="controls">
183
+ <form id="file-form">
184
+ <input id="open-file-chooser" type="submit" value="Select files to upload"/>
185
+ <input id="file-chooser" type="file" multiple="true" />
186
+ </form>
187
+ </div>
188
+ </div>
189
+ """).appendTo '#container'
190
+
191
+ $('#file-chooser').change (event) =>
192
+ @uploads.queue event.target.files
193
+ $('#file-chooser').val ''
194
+
195
+ $('#file-form').submit ->
196
+ $('#file-chooser').click()
197
+ false
198
+
199
+ $('#upload-dnd').bind 'dragenter', (event) ->
200
+ event.stopPropagation()
201
+ event.preventDefault()
202
+ $('#upload-dnd').css 'color', '#444'
203
+
204
+ $('#upload-dnd').bind 'dragleave', (event) ->
205
+ $('#upload-dnd').css 'color', '#ababab'
206
+
207
+ $('#upload-dnd').bind 'dragover', (event) ->
208
+ event.stopPropagation()
209
+ event.preventDefault()
210
+
211
+ $('#upload-dnd').bind 'drop', (event) =>
212
+ event.stopPropagation()
213
+ event.preventDefault()
214
+ $('#upload-dnd').css 'color', '#ababab'
215
+ @uploads.queue event.originalEvent.dataTransfer.files
216
+
217
+ this.findLabels()
218
+ this.findFiles()
219
+
220
+ $('#container').show()
221
+ layout = this.resize()
222
+
223
+ fn = ->
224
+ layout.resize()
225
+ layout.resize() # not sure why two are needed
226
+
227
+ new Filter
228
+ list: '#files'
229
+ icon: '#search-files-icon'
230
+ form: '#search-files-form'
231
+ attrs: ['data-name', 'data-created']
232
+ open: fn
233
+ close: fn
234
+
235
+ resize: ->
236
+ a = $ '#alpha'
237
+ b = $ '#beta'
238
+ c = $ '#charlie'
239
+ up = $ '#uploads'
240
+ dnd = $ '#upload-dnd'
241
+ new Layout ->
242
+ c.css 'left', a.width() + b.width()
243
+ dnd.height up.height()
244
+ dnd.css 'line-height', up.height() + 'px'
245
+
246
+ class Uploads
247
+ constructor: (options) ->
248
+ @session = options.session
249
+ @serviceJid = options.jid
250
+ @size = options.size
251
+ @complete = options.complete
252
+ @uploads = []
253
+ @sending = null
254
+
255
+ queue: (files) ->
256
+ this.add file for file in files when not this.find file
257
+ this.process()
258
+
259
+ add: (file) ->
260
+ node = this.node file
261
+ meter = $ '.meter', node
262
+ @uploads.push new Transfer
263
+ to: @serviceJid()
264
+ file: file
265
+ session: @session
266
+ progress: (pct) ->
267
+ meter.css 'width', pct + '%'
268
+ complete: =>
269
+ this.remove file
270
+ this.complete file if this.complete
271
+ @sending = null if file.name == @sending.name
272
+ this.process()
273
+
274
+ process: ->
275
+ return if @sending
276
+ if upload = @uploads[0]
277
+ @sending = upload.file
278
+ upload.start()
279
+ else
280
+ @sending = null
281
+ this.fileNode(upload.file)
282
+
283
+ find: (file) ->
284
+ (up for up in @uploads when up.file.name == file.name).shift()
285
+
286
+ remove: (file) ->
287
+ @uploads = (up for up in @uploads when up.file.name != file.name)
288
+ node = $ "#uploads li[data-file='#{file.name}']"
289
+ node.fadeOut 200, -> node.remove()
290
+
291
+ node: (file) ->
292
+ node = $("""
293
+ <li data-file="" style="display:none;">
294
+ <form class="inset">
295
+ <h2></h2>
296
+ <div class="progress">
297
+ <div class="meter"></div>
298
+ <span class="text">#{this.size file.size}</span>
299
+ <div class="cancel"></div>
300
+ </div>
301
+ </form>
302
+ </li>
303
+ """).appendTo '#uploads'
304
+ node.fadeIn 200
305
+ $('h2', node).text file.name
306
+ node.attr 'data-file', file.name
307
+
308
+ new Button $('.cancel', node).get(0), ICONS.cross,
309
+ translation: '-8 -8'
310
+ scale: 0.5
311
+ $('.cancel', node).click => this.cancel file
312
+ node
313
+
314
+ cancel: (file) ->
315
+ upload.stop() if upload = this.find file
@@ -0,0 +1,21 @@
1
+ $ ->
2
+ session = new Session()
3
+ nav = new NavBar(session)
4
+ nav.draw()
5
+ buttons =
6
+ Systems: ICONS.commandline
7
+ Services: ICONS.magic
8
+ Files: ICONS.page2
9
+ Setup: ICONS.gear2
10
+ Logout: ICONS.power
11
+ nav.addButton(label, icon) for label, icon of buttons
12
+
13
+ pages =
14
+ '/systems': new SystemsPage(session)
15
+ '/services': new ServicesPage(session)
16
+ '/files': new FilesPage(session)
17
+ '/setup': new SetupPage(session)
18
+ '/logout': new LogoutPage(session)
19
+ 'default': new LoginPage(session, '/systems/')
20
+ new Router(pages).draw()
21
+ nav.select $('#nav-link-systems').parent()
@@ -0,0 +1,356 @@
1
+ class ServicesPage
2
+ SERVICES = 'http://getvines.com/protocol/services'
3
+ MEMBERS = 'http://getvines.com/protocol/services/members'
4
+ SYSTEMS = 'http://getvines.com/protocol/systems'
5
+ ATTRS = 'http://getvines.com/protocol/systems/attributes'
6
+ USERS = 'http://getvines.com/protocol/users'
7
+
8
+ constructor: (@session) ->
9
+ @api = new Api @session
10
+ @selectedService = null
11
+ @validateTimeout = null
12
+ @layout = null
13
+ @users = []
14
+
15
+ deleteService: (event) ->
16
+ this.drawBlankSlate()
17
+ this.toggleForm '#remove-contact-form'
18
+ selected = $("#services li[data-id='#{@selectedService.id}']")
19
+ @api.remove SERVICES, @selectedService.id, (result) =>
20
+ selected.fadeOut 200, ->
21
+ selected.remove()
22
+ @selectedService = null
23
+ false
24
+
25
+ icon: (member) ->
26
+ icons =
27
+ darwin: 'mac.png'
28
+ linux: 'linux.png'
29
+ windows: 'windows.png'
30
+ icon = icons[member.os] || 'run.png'
31
+ "images/#{icon}"
32
+
33
+ drawMember: (member) ->
34
+ return unless this.editorVisible()
35
+ node = $("""
36
+ <li>
37
+ <span class="icon"><img src="#{this.icon(member)}"/></span>
38
+ <span class="text"></span>
39
+ </li>
40
+ """).appendTo '#members'
41
+ $('.text', node).text member.name
42
+
43
+ operators: ->
44
+ for operator in ['like', 'not like', 'starts with', 'ends with', 'is', 'is not', '>', '>=', '<', '<=', 'and', 'or']
45
+ node = $("""
46
+ <li data-selector="#{operator}">
47
+ #{operator}
48
+ </li>
49
+ """).appendTo '#operators'
50
+ node.click (event) =>
51
+ $('#syntax').focus()
52
+ name = $(event.currentTarget).attr 'data-selector'
53
+ $('#syntax').val($('#syntax').val() + " #{name} ")
54
+ this.validateIn()
55
+
56
+ selectService: (node) ->
57
+ id = $(node).attr 'data-id'
58
+ name = $(node).attr 'data-name'
59
+
60
+ $('#services li').removeClass 'selected'
61
+ $(node).addClass 'selected'
62
+
63
+ $('#remove-service-msg').html "Are you sure you want to remove the " +
64
+ "<strong>#{name}</strong> service?"
65
+ $('#remove-service-form .buttons').fadeIn 200
66
+ @api.get SERVICES, id: id, (result) =>
67
+ @selectedService = result
68
+ this.drawEditor(result)
69
+
70
+ serviceNode: (service) ->
71
+ label = if service.size == 1 then 'system' else 'systems'
72
+ node = $("""
73
+ <li data-id="#{service.id}" data-name="" data-size="#{service.size}">
74
+ <span class="text">#{service.name}</span>
75
+ <span class="count">#{service.size} #{label}</span>
76
+ </li>
77
+ """).appendTo '#services'
78
+ node.attr 'data-name', service.name
79
+ $('.text', node).text service.name
80
+ node.click (event) => this.selectService event.currentTarget
81
+ node
82
+
83
+ findServices: ->
84
+ @api.get SERVICES, {}, (result) =>
85
+ this.serviceNode row for row in result.rows
86
+
87
+ findMembers: (id) ->
88
+ @api.get MEMBERS, id: id, (result) =>
89
+ this.drawMember row for row in result.rows
90
+
91
+ findUsers: (syntax) ->
92
+ @api.get USERS, {}, (result) =>
93
+ @users = (row for row in result.rows when !row.system)
94
+ this.drawUsers()
95
+
96
+ attributeNode: (attribute) ->
97
+ node = $('<li data-name=""></li>').appendTo '#attributes'
98
+ node.text attribute
99
+ node.attr 'data-name', attribute
100
+ node.click (event) =>
101
+ $('#syntax').focus()
102
+ name = $(event.currentTarget).attr 'data-name'
103
+ $('#syntax').val($('#syntax').val() + " #{name} ")
104
+ this.validateIn()
105
+
106
+ findAttributes: (syntax) ->
107
+ @api.get ATTRS, {}, (result) =>
108
+ this.attributeNode row for row in result.rows
109
+
110
+ validateIn: (millis)->
111
+ clearTimeout @validateTimeout
112
+ @validateTimeout = setTimeout (=> this.validate()), millis || 500
113
+
114
+ validate: ->
115
+ $('#syntax-status').text ''
116
+
117
+ # only validate if text changed
118
+ prev = $('#syntax').data 'prev'
119
+ code = $.trim $('#syntax').val()
120
+ $('#syntax').data 'prev', code
121
+ return unless code && code != prev
122
+
123
+ $('#syntax-status').text 'Searching . . .'
124
+ $('#members').empty()
125
+ @api.get2 MEMBERS, code, (result) =>
126
+ if result.ok
127
+ $('#syntax-status').text ''
128
+ this.drawMember row for row in result.rows
129
+ else
130
+ $('#syntax-status').text result.error
131
+
132
+ validateForm: ->
133
+ $('#name-error').empty()
134
+ valid = true
135
+
136
+ name = $.trim $('#name').val()
137
+ if name == ''
138
+ $('#name-error').text 'Name is required.'
139
+ valid = false
140
+
141
+ valid
142
+
143
+ save: ->
144
+ return unless this.validateForm()
145
+ users = $('#users :checked').map(-> $(this).val()).get()
146
+ accounts = $('#unix-users').val().split(',')
147
+ accounts = ($.trim u for u in accounts when $.trim(u).length > 0)
148
+ service =
149
+ name: $('#name').val()
150
+ code: $('#syntax').val()
151
+ accounts: accounts
152
+ users: users
153
+ service['id'] = $('#id').val() if $('#id').val().length > 0
154
+
155
+ @api.save SERVICES, service, (result) =>
156
+ new Notification 'Service saved successfully'
157
+ result.size = $('#members').length
158
+ $('#id').val result.id
159
+ node = $("#services li[data-id='#{result.id}']")
160
+ if node.length == 0
161
+ node = this.serviceNode result
162
+ this.selectService node
163
+ else
164
+ $('.text', node).text result.name
165
+ false
166
+
167
+ drawBlankSlate: ->
168
+ $('#beta').empty()
169
+ $("""
170
+ <form id="blank-slate">
171
+ <p>
172
+ Services are dynamically updated groups of systems based on
173
+ criteria you define. Send a command to the service and it runs
174
+ on every system in the group.
175
+ </p>
176
+ <input type="submit" id="blank-slate-add" value="Create a New Service"/>
177
+ </form>
178
+ """).appendTo '#beta'
179
+ $('#blank-slate').submit =>
180
+ this.drawEditor()
181
+ false
182
+
183
+ draw: ->
184
+ unless @session.connected()
185
+ window.location.hash = ''
186
+ return
187
+
188
+ $('body').attr 'id', 'services-page'
189
+ $('#container').hide().empty()
190
+ $("""
191
+ <div id="alpha" class="sidebar column y-fill">
192
+ <h2>Services <div id="search-services-icon"></div></h2>
193
+ <div id="search-services-form"></div>
194
+ <ul id="services" class="selectable scroll y-fill"></ul>
195
+ <div id="alpha-controls" class="controls">
196
+ <div id="add-service"></div>
197
+ <div id="remove-service"></div>
198
+ </div>
199
+ <form id="remove-service-form" class="overlay" style="display:none;">
200
+ <h2>Remove Service</h2>
201
+ <p id="remove-service-msg">Select a service in the list above to remove.</p>
202
+ <fieldset class="buttons" style="display:none;">
203
+ <input id="remove-service-cancel" type="button" value="Cancel"/>
204
+ <input id="remove-service-ok" type="submit" value="Remove"/>
205
+ </fieldset>
206
+ </form>
207
+ </div>
208
+ <div id="beta" class="primary column x-fill y-fill"></div>
209
+ <div id="charlie" class="sidebar column y-fill">
210
+ <h2>Operators</h2>
211
+ <ul id="operators"></ul>
212
+ <h2>Attributes <div id="search-attributes-icon"></div></h2>
213
+ <div id="search-attributes-form"></div>
214
+ <ul id="attributes" class="y-fill scroll"></ul>
215
+ </div>
216
+ """).appendTo '#container'
217
+
218
+ new Button '#add-service', ICONS.plus
219
+ new Button '#remove-service', ICONS.minus
220
+
221
+ this.drawBlankSlate()
222
+
223
+ $('#add-service').click => this.drawEditor()
224
+ $('#remove-service').click => this.toggleForm '#remove-service-form'
225
+ $('#remove-service-cancel').click => this.toggleForm '#remove-service-form'
226
+ $('#remove-service-form').submit => this.deleteService()
227
+
228
+ this.operators()
229
+ this.findServices()
230
+ this.findAttributes()
231
+ this.findUsers()
232
+
233
+ $('#container').show()
234
+ @layout = this.resize()
235
+
236
+ fn = =>
237
+ @layout.resize()
238
+ @layout.resize() # not sure why two are needed
239
+
240
+ new Filter
241
+ list: '#services'
242
+ icon: '#search-services-icon'
243
+ form: '#search-services-form'
244
+ attrs: ['data-name']
245
+ open: fn
246
+ close: fn
247
+
248
+ new Filter
249
+ list: '#attributes'
250
+ icon: '#search-attributes-icon'
251
+ form: '#search-attributes-form'
252
+ attrs: ['data-name']
253
+ open: fn
254
+ close: fn
255
+
256
+ drawEditor: (service) ->
257
+ return unless this.pageVisible()
258
+
259
+ unless service
260
+ @selectedService = null
261
+ $('#services li').removeClass 'selected'
262
+
263
+ $('#beta').empty()
264
+ $("""
265
+ <form id="editor-form" class="sections y-fill scroll">
266
+ <input id="id" type="hidden"/>
267
+ <div>
268
+ <section>
269
+ <h2>Service</h2>
270
+ <fieldset>
271
+ <label for="name">Name</label>
272
+ <input id="name" type="text"/>
273
+ <p id="name-error" class="error"></p>
274
+ <label for="syntax">Criteria</label>
275
+ <textarea id="syntax" placeholder="fqdn like 'www.*' and platform in ['fedora', 'mac_os_x']"></textarea>
276
+ <p id="syntax-status"></p>
277
+ </fieldset>
278
+ </section>
279
+ <section>
280
+ <h2>Members</h2>
281
+ <fieldset id="service-preview">
282
+ <ul id="members" class="scroll"></ul>
283
+ </fieldset>
284
+ </section>
285
+ <section>
286
+ <h2>Permissions</h2>
287
+ <fieldset>
288
+ <label>Users</label>
289
+ <ul id="users" class="scroll"></ul>
290
+ <label for="unix-users">Unix Accounts</label>
291
+ <input id="unix-users" type="text"/>
292
+ </fieldset>
293
+ </section>
294
+ </div>
295
+ </form>
296
+ <form id="editor-buttons">
297
+ <input id="save" type="submit" value="Save"/>
298
+ </form>
299
+ """).appendTo '#beta'
300
+
301
+ if service
302
+ this.findMembers(service.id)
303
+ $('#id').val service.id
304
+ $('#name').val service.name
305
+ $('#syntax').val service.code
306
+ $('#unix-users').val service.accounts.join(', ')
307
+
308
+ this.drawUsers() if @users.length > 0
309
+
310
+ @layout.resize()
311
+ $('#name').focus()
312
+
313
+ $('#syntax').change => this.validateIn()
314
+ $('#syntax').keyup => this.validateIn()
315
+ $('#editor-form').submit => this.save()
316
+ $('#editor-buttons').submit => this.save()
317
+
318
+ drawUsers: ->
319
+ return unless this.editorVisible()
320
+
321
+ $('#users').empty()
322
+ for user in @users
323
+ node = $("""
324
+ <li>
325
+ <input id='user-#{user.jid}' type='checkbox' value='#{user.jid}'/>
326
+ <label for='user-#{user.jid}'>#{user.jid}</label>
327
+ </li>
328
+ """).appendTo '#users'
329
+ # user creating service gets access to it by default
330
+ $('input', node).prop 'checked', true if user.jid == @session.bareJid()
331
+
332
+ if @selectedService
333
+ $('#users input[type="checkbox"]').val @selectedService.users
334
+
335
+ toggleForm: (form, fn) ->
336
+ form = $(form)
337
+ $('form.overlay').each ->
338
+ $(this).hide() unless this.id == form.attr 'id'
339
+ if form.is ':hidden'
340
+ fn() if fn
341
+ form.fadeIn 100
342
+ else
343
+ form.fadeOut 100, ->
344
+ form[0].reset()
345
+ fn() if fn
346
+
347
+ pageVisible: -> $('#services-page').length > 0
348
+
349
+ editorVisible: -> $('#services-page #editor-form').length > 0
350
+
351
+ resize: ->
352
+ a = $ '#alpha'
353
+ b = $ '#beta'
354
+ c = $ '#charlie'
355
+ new Layout ->
356
+ c.css 'left', a.width() + b.width()