frontline 0.0.7

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