frontline 0.0.7

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 (55) hide show
  1. data/CHANGELOG.md +0 -0
  2. data/LICENSE +19 -0
  3. data/README.md +92 -0
  4. data/Rakefile +21 -0
  5. data/assets/ansi_up.js +143 -0
  6. data/assets/api.js +533 -0
  7. data/assets/bootstrap/css/bootstrap-responsive.min.css +9 -0
  8. data/assets/bootstrap/css/bootstrap.min.css +9 -0
  9. data/assets/bootstrap/img/glyphicons-halflings-white.png +0 -0
  10. data/assets/bootstrap/img/glyphicons-halflings.png +0 -0
  11. data/assets/bootstrap/js/bootstrap.min.js +6 -0
  12. data/assets/jquery.cookie.js +95 -0
  13. data/assets/jquery.js +6 -0
  14. data/assets/noty/jquery.noty.js +520 -0
  15. data/assets/noty/layouts/top.js +34 -0
  16. data/assets/noty/layouts/topRight.js +43 -0
  17. data/assets/noty/promise.js +432 -0
  18. data/assets/noty/themes/default.js +156 -0
  19. data/assets/select2-bootstrap.css +86 -0
  20. data/assets/select2/select2-spinner.gif +0 -0
  21. data/assets/select2/select2.css +615 -0
  22. data/assets/select2/select2.min.js +22 -0
  23. data/assets/select2/select2.png +0 -0
  24. data/assets/select2/select2x2.png +0 -0
  25. data/assets/typeahead.js-bootstrap.css +49 -0
  26. data/assets/typeahead.min.js +7 -0
  27. data/assets/ui.css +28 -0
  28. data/assets/xhr.js +19 -0
  29. data/bin/frontline +19 -0
  30. data/frontline.gemspec +31 -0
  31. data/images/0.png +0 -0
  32. data/lib/frontline.rb +23 -0
  33. data/lib/frontline/actions.rb +15 -0
  34. data/lib/frontline/app.rb +86 -0
  35. data/lib/frontline/controllers/controllers.rb +71 -0
  36. data/lib/frontline/controllers/index.rb +167 -0
  37. data/lib/frontline/controllers/models.rb +104 -0
  38. data/lib/frontline/controllers/sources.rb +11 -0
  39. data/lib/frontline/frontline.rb +11 -0
  40. data/lib/frontline/helpers.rb +179 -0
  41. data/lib/frontline/inflect.rb +183 -0
  42. data/lib/frontline/templates/controllers/controller.slim +27 -0
  43. data/lib/frontline/templates/controllers/index.slim +56 -0
  44. data/lib/frontline/templates/controllers/route.slim +17 -0
  45. data/lib/frontline/templates/controllers/route_editor.slim +45 -0
  46. data/lib/frontline/templates/controllers/route_layout.slim +88 -0
  47. data/lib/frontline/templates/editor.slim +17 -0
  48. data/lib/frontline/templates/error.slim +36 -0
  49. data/lib/frontline/templates/index/applications.slim +123 -0
  50. data/lib/frontline/templates/layout.slim +182 -0
  51. data/lib/frontline/templates/models/index.slim +31 -0
  52. data/lib/frontline/templates/models/migration.slim +46 -0
  53. data/lib/frontline/templates/models/migration_layout.slim +159 -0
  54. data/lib/frontline/templates/models/model.slim +34 -0
  55. metadata +245 -0
@@ -0,0 +1,104 @@
1
+ class Frontline
2
+ class Models < E
3
+ reject_automount!
4
+
5
+ # runs before new model generated.
6
+ # converts throughFor Hash, if any, into string options suitable for Enginery.
7
+ before :post_model do
8
+ (params.delete('throughFor') || {}).each_pair do |a,t|
9
+ next if (assoc = params[a].to_s.strip).empty? || (through = t.strip).empty?
10
+ params[a] = [assoc, 'through', through]*':'
11
+ end
12
+ end
13
+
14
+ # return a command that will generate a new model.
15
+ # `@name` and `@setups` variables are set by the triggered hooks(see frontline/app.rb for hooks)
16
+ def post_model
17
+ 'generate:model %s %s' % [@name, @setups]
18
+ end
19
+
20
+ # return a command that will delete the given model.
21
+ # `@name` variable are set by the triggered hooks(see frontline/app.rb for hooks)
22
+ def delete_model
23
+ 'delete:model:yes %s' % @name
24
+ end
25
+
26
+ # return a command that will generate a new migration for given model.
27
+ # `@name` and `@setups` variables are set by the triggered hooks(see frontline/app.rb for hooks)
28
+ def post_migration model
29
+ 'm %s model:%s %s' % [@name, model, @setups]
30
+ end
31
+
32
+ # return a command that will delete the given migration of given model.
33
+ # `@name` variable are set by the triggered hooks(see frontline/app.rb for hooks)
34
+ def delete_migration model
35
+ 'delete:migration:yes %s' % @name
36
+ end
37
+
38
+ # run given migration or outstanding ones if no migrations given.
39
+ # see `Helpers#pty_stream` for running/streaming details.
40
+ #
41
+ # it requires `:vector` param to be set to "up" or "down".
42
+ # by default it will skip already performed migrations
43
+ # if `:force_run` is set, it will run migrations regardless performed state.
44
+ # if `:force_run` NOT set, `:force_yes` option should be set to true
45
+ # to avoid any confirmation prompts generated by Enginery.
46
+ def post_run_migrations
47
+ @uuid = params[:uuid]
48
+ vector = params[:vector]
49
+ extra = params[:force_run] ? ':f' : (params[:force_yes] ? ':y' : '')
50
+ migrations = (params[:migrations]||[])*' '
51
+ cmd = 'bundle exec enginery m:%s%s %s' % [vector, extra, migrations]
52
+ stream do
53
+ pty_stream 'modal', 'show'
54
+ passed, failure_id = pty_spawn(cmd)
55
+ if passed
56
+ pty_stream 'modal', 'hide'
57
+ else
58
+ pty_stream 'failures', failure_id
59
+ end
60
+ end
61
+ end
62
+
63
+ # run given DataMapper migrate task and stream result to browser.
64
+ # see `Helpers#pty_stream` for running/streaming details.
65
+ def post_run_datamapper_task task, model = nil
66
+
67
+ %w[auto_migrate auto_upgrade].include?(task) ||
68
+ halt(400, 'Unknown DataMapper task "%s"' % escape_html(task))
69
+
70
+ @uuid = params.delete('uuid')
71
+ cmd = 'rake dm:' + task
72
+ model && cmd << ':' << model
73
+
74
+ stream do
75
+ pty_stream 'modal', 'show'
76
+ passed, failure_id = pty_spawn(cmd)
77
+ if passed
78
+ pty_stream 'modal', 'hide'
79
+ else
80
+ pty_stream 'failures', failure_id
81
+ end
82
+ end
83
+ end
84
+
85
+ # boot effective application and get last track for given migration
86
+ def last_run
87
+ Dir.chdir dst_path.root do
88
+ migrator = Enginery::Migrator.new(dst_path.root)
89
+ migrator.boot_app
90
+ if track = migrator.last_run(params[:file])
91
+ '%s on %s' % [track.first.upcase, track.last]
92
+ else
93
+ 'Never'
94
+ end
95
+ end
96
+ end
97
+
98
+ def migration_layout model
99
+ model = models[model] || halt(400, '"%s" model does not exists' % escape_html(model))
100
+ render_p model: model
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,11 @@
1
+ class Frontline
2
+ class Sources < E
3
+ reject_automount!
4
+ include EL::Finder
5
+
6
+ # asking el-finder to generate a file manager for editing effective application
7
+ def index
8
+ render_l { finder(dst_path.root, editor: :ace) }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ class Frontline
2
+ ROOT = (File.expand_path('../../..', __FILE__) + '/').freeze
3
+ TEMPLATES = (ROOT + 'lib/frontline/templates/').freeze
4
+ ASSETS = (ROOT + 'assets/').freeze
5
+ ASSETS_SUFFIX = '-frontline'.freeze
6
+ ASSETS_REGEXP = /#{Regexp.escape ASSETS_SUFFIX}/.freeze
7
+ STREAMS = {}
8
+ CONTROLLERS = {}
9
+ MODELS = {}
10
+ PORT = 5000
11
+ end
@@ -0,0 +1,179 @@
1
+ class Frontline
2
+ module Helpers
3
+ include Inflector
4
+
5
+ # send data to browser via EventSource socket.
6
+ # socket should be earlier set via `Index#get_streamer`.
7
+ # actions calling this helper should firstly set `@uuid` variable.
8
+ #
9
+ # @param [String] event
10
+ # event to be sent to browser
11
+ # @param [String] data
12
+ # data to be sent to browser
13
+ def pty_stream event, data = ''
14
+ return unless stream = STREAMS[@uuid]
15
+ stream.event event
16
+ stream.data data
17
+ end
18
+
19
+ # return the list of managed applications.
20
+ def applications
21
+ (a = cookies['Applications']) ? JSON.load(a) : []
22
+ end
23
+
24
+ # return the list of controllers for effective application
25
+ def controllers
26
+ return CONTROLLERS[dst_path.root] if CONTROLLERS[dst_path.root]
27
+ result = enginery_registry(:c)
28
+ return CONTROLLERS[dst_path.root] = result if result.is_a?(Hash)
29
+ result
30
+ end
31
+
32
+ # return the list of models for effective application
33
+ def models
34
+ return MODELS[dst_path.root] if MODELS[dst_path.root]
35
+ result = enginery_registry(:m)
36
+ return MODELS[dst_path.root] = result if result.is_a?(Hash)
37
+ result
38
+ end
39
+
40
+ # execute a enginery registry command.
41
+ # data are extracted via `$ enginery -c` command so it comes in YAML format.
42
+ # if call was successful, parse the YAML and return the result as a Hash.
43
+ # otherwise return the stdout+stderr as a String.
44
+ # if YAML parser failing, a exception will be raised.
45
+ def enginery_registry unit
46
+ Dir.chdir dst_path.root do
47
+ cmd = '%s -%s' % [Enginery::EXECUTABLE, unit]
48
+ stdout, stderr, status = Open3.capture3(cmd)
49
+ if status && status.exitstatus == 0
50
+ YAML.load(stdout)
51
+ else
52
+ [stdout, stderr].join("\n")
53
+ end
54
+ end
55
+ end
56
+
57
+ def stringify_application name, path, url
58
+ JSON.dump([name, path, url])
59
+ end
60
+
61
+ # running given command via `PTY.spawn` and streaming each line to browser.
62
+ # actions calling this helper should firstly set `@uuid` variable for streaming to work.
63
+ #
64
+ # before executing given cmd it will change working directory
65
+ # to effective application root(see `Index#get_application`).
66
+ #
67
+ # if some exception raised it will be rescued
68
+ # and error message alongside backtrace will be streamed to browser.
69
+ #
70
+ # it returns an Array first element of which is the status
71
+ # and the second is a unique ID used to identify lines generated by given cmd.
72
+ # if status is negative, actions calling this helper will can identify
73
+ # lines and mark them as errored.
74
+ #
75
+ def pty_spawn cmd, opts = {}
76
+ failure_id = 'pty_' << cmd.__id__.to_s
77
+ root = opts[:root] || dst_path.root
78
+ pty_stream 'content', div_tag!('$ cd ' << root, class: 'muted')
79
+ pty_stream 'content', div_tag!(b_tag('$ ' << cmd, class: 'text-info'))
80
+ Dir.chdir root do
81
+ PTY.spawn cmd do |r, w, pid|
82
+ begin
83
+ r.sync
84
+ r.each_line do |line|
85
+ line.rstrip!
86
+
87
+ # div_tag will escape line before emitting it
88
+ html = line.empty? ? br_tag : div_tag(line, class: failure_id)
89
+
90
+ pty_stream 'content', html
91
+
92
+ end
93
+ rescue Errno::EIO # simply ignoring this
94
+ ensure
95
+ ::Process.wait pid
96
+ end
97
+ end
98
+ end
99
+ [$? && $?.exitstatus == 0, failure_id]
100
+ rescue => e
101
+ pty_stream 'content', p_tag(e.message)
102
+ e.backtrace.each {|l| pty_stream('content', div_tag(l))}
103
+ [false, failure_id]
104
+ end
105
+
106
+ def maintenance_files
107
+ files = %w[
108
+ Gemfile
109
+ Rakefile
110
+ base/boot.rb
111
+ config/config.yml
112
+ config/database.yml
113
+ base/helpers/application_helpers.rb
114
+ ]
115
+ Dir[dst_path(:views, 'layout*')].each do |e|
116
+ File.file?(e) && files.unshift('base/views/' + File.basename(e))
117
+ end
118
+ files
119
+ end
120
+
121
+ # determine whether effective application is using DataMapper ORM
122
+ def datamapper?
123
+ (m = models) && m.is_a?(Hash) && (m = models.values.first) && m[:orm].to_s =~ /\Ad/i
124
+ end
125
+
126
+ @@association_hints = {}
127
+ def association_hints
128
+ @@association_hints[models.__id__] ||= models.keys.map {|x| underscore(x.to_s)}
129
+ end
130
+
131
+ def singularized_association_hints
132
+ association_hints.map {|m| singularize(m)}
133
+ end
134
+
135
+ def pluralized_association_hints
136
+ association_hints.map {|m| pluralize(m)}
137
+ end
138
+
139
+ def association_hint_class assoc
140
+ assoc == :belongs_to || assoc == :has_one ?
141
+ 'singularized_association_hints' :
142
+ 'pluralized_association_hints'
143
+ end
144
+
145
+ def clear_registry_cache!
146
+ CONTROLLERS.clear
147
+ MODELS.clear
148
+ end
149
+
150
+ def postcrud_handlers
151
+ cache = if action_name[0] == 'm'
152
+ MODELS.delete(dst_path.root)
153
+ models()
154
+ else
155
+ CONTROLLERS.delete(dst_path.root)
156
+ controllers()
157
+ end
158
+
159
+ errors = []
160
+ if cache.is_a?(Hash)
161
+ begin
162
+ pty_stream 'render', render_p(action_name, action_params)
163
+ pty_stream 'modal', 'hide'
164
+ rescue => e
165
+ errors = [e.message] + e.backtrace
166
+ end
167
+ else
168
+ errors = cache.to_s.split("\n")
169
+ end
170
+ if errors.any?
171
+ html = hr_tag << div_tag(class: 'alert alert-error lead') {
172
+ 'Error loading %ss' % action_name
173
+ } << errors.map {|e| div_tag(e, class: 'text-error')}.join
174
+ pty_stream('content', html)
175
+ end
176
+ end
177
+
178
+ end
179
+ end
@@ -0,0 +1,183 @@
1
+ class Frontline
2
+ module Inflector
3
+ extend self
4
+
5
+ class Inflections
6
+ @__instance__ = {}
7
+
8
+ def self.instance(locale = :en)
9
+ @__instance__[locale] ||= new
10
+ end
11
+
12
+ attr_reader :plurals, :singulars, :uncountables
13
+
14
+ def initialize
15
+ @plurals, @singulars, @uncountables = [], [], []
16
+ end
17
+
18
+ # Specifies a new pluralization rule and its replacement. The rule can
19
+ # either be a string or a regular expression. The replacement should
20
+ # always be a string that may include references to the matched data from
21
+ # the rule.
22
+ def plural(rule, replacement)
23
+ @uncountables.delete(rule) if rule.is_a?(String)
24
+ @uncountables.delete(replacement)
25
+ @plurals.unshift([rule, replacement])
26
+ end
27
+
28
+ # Specifies a new singularization rule and its replacement. The rule can
29
+ # either be a string or a regular expression. The replacement should
30
+ # always be a string that may include references to the matched data from
31
+ # the rule.
32
+ def singular(rule, replacement)
33
+ @uncountables.delete(rule) if rule.is_a?(String)
34
+ @uncountables.delete(replacement)
35
+ @singulars.unshift([rule, replacement])
36
+ end
37
+
38
+ # Add uncountable words that shouldn't be attempted inflected.
39
+ #
40
+ # uncountable 'money'
41
+ # uncountable 'money', 'information'
42
+ # uncountable %w( money information rice )
43
+ def uncountable(*words)
44
+ (@uncountables << words).flatten!
45
+ end
46
+ end
47
+
48
+ # Yields a singleton instance of Inflector::Inflections so you can specify
49
+ # additional inflector rules. If passed an optional locale, rules for other
50
+ # languages can be specified. If not specified, defaults to <tt>:en</tt>.
51
+ # Only rules for English are provided.
52
+ #
53
+ # ActiveSupport::Inflector.inflections(:en) do |inflect|
54
+ # inflect.uncountable 'rails'
55
+ # end
56
+ def inflections(locale = :en)
57
+ if block_given?
58
+ yield Inflections.instance(locale)
59
+ else
60
+ Inflections.instance(locale)
61
+ end
62
+ end
63
+ end
64
+
65
+ Inflector.inflections(:en) do |inflect|
66
+ inflect.plural(/$/, 's')
67
+ inflect.plural(/s$/i, 's')
68
+ inflect.plural(/^(ax|test)is$/i, '\1es')
69
+ inflect.plural(/(octop|vir)us$/i, '\1i')
70
+ inflect.plural(/(octop|vir)i$/i, '\1i')
71
+ inflect.plural(/(alias|status)$/i, '\1es')
72
+ inflect.plural(/(bu)s$/i, '\1ses')
73
+ inflect.plural(/(buffal|tomat)o$/i, '\1oes')
74
+ inflect.plural(/([ti])um$/i, '\1a')
75
+ inflect.plural(/([ti])a$/i, '\1a')
76
+ inflect.plural(/sis$/i, 'ses')
77
+ inflect.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
78
+ inflect.plural(/(hive)$/i, '\1s')
79
+ inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies')
80
+ inflect.plural(/(x|ch|ss|sh)$/i, '\1es')
81
+ inflect.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices')
82
+ inflect.plural(/^(m|l)ouse$/i, '\1ice')
83
+ inflect.plural(/^(m|l)ice$/i, '\1ice')
84
+ inflect.plural(/^(ox)$/i, '\1en')
85
+ inflect.plural(/^(oxen)$/i, '\1')
86
+ inflect.plural(/(quiz)$/i, '\1zes')
87
+
88
+ inflect.singular(/s$/i, '')
89
+ inflect.singular(/(ss)$/i, '\1')
90
+ inflect.singular(/(n)ews$/i, '\1ews')
91
+ inflect.singular(/([ti])a$/i, '\1um')
92
+ inflect.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '\1sis')
93
+ inflect.singular(/(^analy)(sis|ses)$/i, '\1sis')
94
+ inflect.singular(/([^f])ves$/i, '\1fe')
95
+ inflect.singular(/(hive)s$/i, '\1')
96
+ inflect.singular(/(tive)s$/i, '\1')
97
+ inflect.singular(/([lr])ves$/i, '\1f')
98
+ inflect.singular(/([^aeiouy]|qu)ies$/i, '\1y')
99
+ inflect.singular(/(s)eries$/i, '\1eries')
100
+ inflect.singular(/(m)ovies$/i, '\1ovie')
101
+ inflect.singular(/(x|ch|ss|sh)es$/i, '\1')
102
+ inflect.singular(/^(m|l)ice$/i, '\1ouse')
103
+ inflect.singular(/(bus)(es)?$/i, '\1')
104
+ inflect.singular(/(o)es$/i, '\1')
105
+ inflect.singular(/(shoe)s$/i, '\1')
106
+ inflect.singular(/(cris|test)(is|es)$/i, '\1is')
107
+ inflect.singular(/^(a)x[ie]s$/i, '\1xis')
108
+ inflect.singular(/(octop|vir)(us|i)$/i, '\1us')
109
+ inflect.singular(/(alias|status)(es)?$/i, '\1')
110
+ inflect.singular(/^(ox)en/i, '\1')
111
+ inflect.singular(/(vert|ind)ices$/i, '\1ex')
112
+ inflect.singular(/(matr)ices$/i, '\1ix')
113
+ inflect.singular(/(quiz)zes$/i, '\1')
114
+ inflect.singular(/(database)s$/i, '\1')
115
+
116
+ inflect.uncountable(%w(equipment information rice money species series fish sheep jeans police))
117
+ end
118
+
119
+ module Inflector
120
+ extend self
121
+
122
+ # Returns the plural form of the word in the string.
123
+ #
124
+ # If passed an optional +locale+ parameter, the word will be
125
+ # pluralized using rules defined for that language. By default,
126
+ # this parameter is set to <tt>:en</tt>.
127
+ #
128
+ # 'post'.pluralize # => "posts"
129
+ # 'octopus'.pluralize # => "octopi"
130
+ # 'sheep'.pluralize # => "sheep"
131
+ # 'words'.pluralize # => "words"
132
+ # 'CamelOctopus'.pluralize # => "CamelOctopi"
133
+ # 'ley'.pluralize(:es) # => "leyes"
134
+ def pluralize(word, locale = :en)
135
+ apply_inflections(word, inflections(locale).plurals)
136
+ end
137
+
138
+ # The reverse of +pluralize+, returns the singular form of a word in a
139
+ # string.
140
+ #
141
+ # If passed an optional +locale+ parameter, the word will be
142
+ # pluralized using rules defined for that language. By default,
143
+ # this parameter is set to <tt>:en</tt>.
144
+ #
145
+ # 'posts'.singularize # => "post"
146
+ # 'octopi'.singularize # => "octopus"
147
+ # 'sheep'.singularize # => "sheep"
148
+ # 'word'.singularize # => "word"
149
+ # 'CamelOctopi'.singularize # => "CamelOctopus"
150
+ # 'leyes'.singularize(:es) # => "ley"
151
+ def singularize(word, locale = :en)
152
+ apply_inflections(word, inflections(locale).singulars)
153
+ end
154
+
155
+ def underscore(camel_cased_word)
156
+ word = camel_cased_word.to_s.dup
157
+ word.gsub!('::', '/')
158
+ word.gsub!(/(?:([A-Za-z\d])|^)((?=a)b)(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" }
159
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
160
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
161
+ word.tr!("-", "_")
162
+ word.downcase!
163
+ word
164
+ end
165
+
166
+ private
167
+
168
+ # Applies inflection rules for +singularize+ and +pluralize+.
169
+ #
170
+ # apply_inflections('post', inflections.plurals) # => "posts"
171
+ # apply_inflections('posts', inflections.singulars) # => "post"
172
+ def apply_inflections(word, rules)
173
+ result = word.to_s.dup
174
+ if word.empty? || inflections.uncountables.include?(result.downcase[/\b\w+\Z/])
175
+ result
176
+ else
177
+ rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
178
+ result
179
+ end
180
+ end
181
+
182
+ end
183
+ end