vines-services 0.1.0

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 (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()