rails_db_browser 0.0.9

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.
@@ -0,0 +1,3 @@
1
+ pkg
2
+ *.swp
3
+
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Sokolov Yura aka funny_falcon
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,60 @@
1
+ = RailsDbBrowser
2
+
3
+ Simple database browser for Rails application backed by ActiveRecord
4
+
5
+ == Instalation
6
+
7
+ === Rails 2.3
8
+
9
+ in config/environment.rb
10
+
11
+ config.gem 'rails_db_browser'
12
+
13
+ and then create an app/metal/db_browse.rb
14
+
15
+ DbBrowse = RailsDbBrowser::Runner.new('/db_browse')
16
+
17
+ === Rails 3
18
+
19
+ in Rails 3 in Gemfile
20
+
21
+ gem 'rails_db_browser'
22
+
23
+ in config/routes.rb
24
+
25
+ match "db_browse(/*s)", :to => RailsDbBrowser::Runner.new('/db_browse')
26
+
27
+ == Security
28
+
29
+ It is up to you to provide security.
30
+
31
+ You could check environment and run browser only in development.
32
+ If you user Rails 2.3 then you still should provide empty Rack application as metal
33
+
34
+ class DbBrowse
35
+ def self.call(env)
36
+ [404, nil, nil]
37
+ end
38
+ end
39
+
40
+ You could use Rack::Builder with combination of any Rack authentication middleware
41
+
42
+ DbBrowse = Rack::Builder.new do
43
+ use RailsDbBrowser::URLTruncate, '/db_browse'
44
+ use Rack::Auth::Basic, 'db_browser' do |user, password|
45
+ user == 'admin' && password == 'iamgod'
46
+ end
47
+ run RailsDbBrowser::DbBrowser
48
+ end
49
+
50
+ (Well, I've tested it in Rails2.3. Rails3 application with Devise falls on wrong password)
51
+
52
+ == Repository
53
+
54
+ Source is hosted on github
55
+
56
+ http://github.com/funny-falcon/rails_db_browser
57
+
58
+ == Copyright
59
+
60
+ Copyright (c) 2010 Sokolov Yura aka funny_falcon, released under the MIT license
@@ -0,0 +1,28 @@
1
+ require 'rake'
2
+ require 'rake/rdoctask'
3
+
4
+ desc 'Generate documentation for the db_browser plugin.'
5
+ Rake::RDocTask.new(:rdoc) do |rdoc|
6
+ rdoc.rdoc_dir = 'rdoc'
7
+ rdoc.title = 'DbBrowser'
8
+ rdoc.options << '--line-numbers' << '--inline-source'
9
+ rdoc.rdoc_files.include('README')
10
+ rdoc.rdoc_files.include('lib/**/*.rb')
11
+ end
12
+
13
+ begin
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gemspec|
16
+ gemspec.name = "rails_db_browser"
17
+ gemspec.summary = "Simple database browser for Rails application backed by ActiveRecord"
18
+ gemspec.description = "Simple sinatra Rack application that allowes to run sql queries from a web page. Usable for administrative tasks."
19
+ gemspec.email = "funny.falcon@gmail.com"
20
+ gemspec.homepage = "http://github.com/funny-falcon/rails_db_browser"
21
+ gemspec.authors = ["Sokolov Yura aka funny_falcon"]
22
+ gemspec.add_dependency('sinatra')
23
+ gemspec.add_dependency('haml')
24
+ end
25
+ Jeweler::GemcutterTasks.new
26
+ rescue LoadError
27
+ puts "Jeweler not available. Install it with: gem install jeweler"
28
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.9
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ # Include hook code here
2
+ require 'rails_db_browser'
@@ -0,0 +1,17 @@
1
+ # RailsDbBrowser
2
+ require 'sinatra/base'
3
+ require 'rails_db_browser/url_truncate'
4
+ require 'rails_db_browser/connection_keeper'
5
+ require 'rails_db_browser/db_browser'
6
+
7
+ module RailsDbBrowser
8
+ class Runner
9
+ def initialize(path)
10
+ @url_truncate = URLTruncate.new(DbBrowser, path)
11
+ end
12
+
13
+ def call(env)
14
+ @url_truncate.call(env)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,169 @@
1
+ module RailsDbBrowser
2
+ # Abstract class holding connection staff
3
+ class ConnectionKeeper
4
+ # get connection names
5
+ def connection_names
6
+ ActiveRecord::Base.configurations.keys
7
+ end
8
+
9
+ def connection(name=nil)
10
+ underlying = unless name.present?
11
+ ActiveRecord::Base.connection
12
+ else
13
+ FakeModel.get_connection(name)
14
+ end
15
+ Connection.new(underlying)
16
+ end
17
+
18
+ class FakeModel < ActiveRecord::Base
19
+ @abstract_class = true
20
+ CONNECTS = {}
21
+ def self.get_connection(name)
22
+ CONNECTS[name] ||= begin
23
+ establish_connection(name)
24
+ connection
25
+ end
26
+ end
27
+ end
28
+
29
+ # performs common operations on connection
30
+ class Connection
31
+ attr_accessor :connection
32
+ delegate :quote_table_name, :quote_column_name, :quote,
33
+ :select_value, :select_all,
34
+ :update, :insert, :delete,
35
+ :add_limit_offset!,
36
+ :to => :connection
37
+
38
+ def initialize(connection)
39
+ @connection = connection
40
+ end
41
+
42
+ # getting list of column definitions
43
+ # and order them to be more human readable
44
+ def columns(table)
45
+ columns = get_column_definitions(table)
46
+ columns.sort_by{|c|
47
+ [
48
+ fields_to_head.index(c.name) || 1e6,
49
+ -(fields_to_tail.index(c.name) || 1e6),
50
+ c.name
51
+ ]
52
+ }
53
+ end
54
+
55
+ def column_names(table)
56
+ columns(table).map{|c| c.name}
57
+ end
58
+
59
+ def table_names
60
+ @connection.tables.sort
61
+ end
62
+
63
+ # fields to see first
64
+ def fields_to_head
65
+ @fields_to_head ||= %w{id name login value}
66
+ end
67
+
68
+ # fields to see last
69
+ def fields_to_tail
70
+ @fields_to_tail ||= %w{created_at created_on updated_at updated_on}
71
+ end
72
+
73
+ attr_writer :fields_to_head, :fields_to_tail
74
+
75
+ # sort field names in a rezult
76
+ def sort_fields(fields)
77
+ fields = (fields_to_head & fields) | (fields - fields_to_head)
78
+ fields = (fields - fields_to_tail) | (fields_to_tail & fields)
79
+ fields
80
+ end
81
+
82
+ # performs query with appropriate method
83
+ def query(sql, opts={})
84
+ per_page = (opts[:perpage] || nil).to_i
85
+ page = (opts[:page] || 1 ).try(:to_i)
86
+ fields = opts[:fields] || nil
87
+ case sql
88
+ when /\s*select/i , /\s*(update|insert|delete).+returning/im
89
+ rez = {:fields => fields}
90
+ if sql =~ /\s*select/i && per_page > 0
91
+ rez[:count] = select_value("select count(*) from (#{sql}) as t").to_i
92
+ rez[:pages] = (rez[:count].to_f / per_page).ceil
93
+ sql = "select * from (#{sql}) as t"
94
+ add_limit_offset!( sql,
95
+ :limit => per_page,
96
+ :offset => per_page * (page - 1))
97
+ end
98
+
99
+ rez[:rows] = select_all( sql )
100
+
101
+ unless rez[:rows].blank?
102
+ rez[:fields] ||= []
103
+ rez[:fields].concat( self.sort_fields(rez[:rows].first.keys) - rez[:fields] )
104
+ end
105
+
106
+ Result.new(rez)
107
+ when /\s*update/i
108
+ Result.new :value => update( sql )
109
+ when /\s*insert/i
110
+ Result.new :value => insert( sql )
111
+ when /\s*delete/i
112
+ Result.new :value => delete( sql )
113
+ end
114
+ rescue StandardError => e
115
+ Result.new :error => e
116
+ end
117
+
118
+ private
119
+ def get_column_definitions(table)
120
+ @connection.columns(table).map{|c|
121
+ Column.new(c.name, c.sql_type, c.default, c.null)
122
+ }
123
+ end
124
+
125
+ end
126
+
127
+ class Column
128
+ attr_accessor :name, :type, :default, :null
129
+ def initialize(name, type, default, null)
130
+ @name = name
131
+ @type = type
132
+ @default = default
133
+ @null = null
134
+ end
135
+ end
136
+
137
+ class Result
138
+ attr_accessor :rows, :count, :pages, :fields, :error
139
+ def initialize(opts={})
140
+ if opts[:value]
141
+ @value = [opts[:value]]
142
+ elsif opts[:rows]
143
+ @rows = opts[:rows]
144
+ @count = opts[:count] || @rows.size
145
+ @pages = opts[:pages] || 1
146
+ @fields = opts[:fields]
147
+ elsif opts[:error]
148
+ @error = opts[:error]
149
+ end
150
+ end
151
+
152
+ def value
153
+ @value && @value[0]
154
+ end
155
+
156
+ def value?
157
+ @value.present?
158
+ end
159
+
160
+ def error?
161
+ @error.present?
162
+ end
163
+
164
+ def rows?
165
+ @rows != nil
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,189 @@
1
+ module RailsDbBrowser
2
+ class TableColumns < Struct.new(:table, :columns)
3
+ end
4
+
5
+ class DbBrowser < Sinatra::Base
6
+ enable :session
7
+ set :views, File.join(File.dirname(__FILE__), '../../views')
8
+ set :public, File.join(File.dirname(__FILE__), '../../public')
9
+ set :connect_keeper, ConnectionKeeper.new
10
+ enable :show_exceptions
11
+
12
+ helpers do
13
+ def logger
14
+ ActiveRecord::Base.logger
15
+ end
16
+
17
+ def keeper
18
+ settings.connect_keeper
19
+ end
20
+
21
+ def connection
22
+ keeper.connection(params[:connection])
23
+ end
24
+
25
+ def connection_names
26
+ keeper.connection_names
27
+ end
28
+
29
+ def url(path, add_params={})
30
+ path = path.sub(%r{/+$},'')
31
+ query = add_params.to_query
32
+ relative = query.present? ? "#{path}?#{query}" : path
33
+ "#{request.script_name}#{relative}"
34
+ end
35
+
36
+ def keep_params(path, add_params={})
37
+ url path, params.slice("connection", "perpage").merge(add_params)
38
+ end
39
+
40
+ def merge_params(add_params)
41
+ query = params.merge(add_params).to_query
42
+ query.present? ? "?#{query}" : ""
43
+ end
44
+
45
+ def connection_field
46
+ haml :_connection_field, :layout => false
47
+ end
48
+
49
+ def per_page_field
50
+ haml :_per_page_field, :layout => false
51
+ end
52
+
53
+ def columns(table)
54
+ connection.columns(table)
55
+ end
56
+
57
+ def column_names(table)
58
+ connection.column_names(table)
59
+ end
60
+
61
+ def extract_tables(fields)
62
+ tables = []
63
+ for field in fields
64
+ if field =~ /(?:([\w\.]+)\.)?(\w+)/
65
+ table, column = $1, $2
66
+ else
67
+ table, column = nil, field
68
+ end
69
+ if !tables.last || tables.last.table != table
70
+ tables << TableColumns.new(table, [column])
71
+ else
72
+ tables.last.columns << column
73
+ end
74
+ end
75
+ tables
76
+ end
77
+
78
+ def inspect_env
79
+ haml <<'HAML', :layout => false
80
+ %pre
81
+ \
82
+ - env.sort.each do |k, v|
83
+ & #{k} = #{v.inspect}
84
+ HAML
85
+ end
86
+
87
+ def table_content_url(table)
88
+ keep_params("",
89
+ :query => "SELECT [[#{table}.*]] FROM #{quote_table_name(table)}\nWHERE 1=1\nORDER BY id")
90
+ end
91
+
92
+ def table_scheme_url(table)
93
+ keep_params("/s/#{table}")
94
+ end
95
+ end
96
+
97
+ def quote_table_name(t)
98
+ connection.quote_table_name(t)
99
+ end
100
+
101
+ def quote_column_name(c)
102
+ connection.quote_column_name(c)
103
+ end
104
+
105
+ def quote(v)
106
+ connection.quote(v)
107
+ end
108
+
109
+ def set_default_perpage
110
+ params['perpage'] = '25' unless params.has_key?('perpage')
111
+ end
112
+
113
+ def extract_fields_from_special(sql)
114
+ fields = []
115
+ sql = sql.gsub(/\[\[([\w.]+)\.\*(?:\s*-\s*((?:\w+[,\s]+)*(?:\w+))\s*)?\]\]/) do
116
+ table, without = $1, ($2 || '').scan(/\w+/)
117
+ table_fields = column_names(table) - without
118
+
119
+ table_fields.map{|fld|
120
+ as = "#{table}.#{fld}"
121
+ fields << as
122
+ "#{quote_table_name(table)}.#{quote_column_name(fld)} as #{quote_column_name(as)}"
123
+ }.join(',')
124
+ end
125
+ [ sql, fields ]
126
+ end
127
+
128
+ def select_all(sql, fields=nil)
129
+ set_default_perpage
130
+ unless fields.present?
131
+ sql, fields = extract_fields_from_special(sql)
132
+ end
133
+ connection.query(sql,
134
+ :perpage => params[:perpage],
135
+ :page => params[:page],
136
+ :fields => fields
137
+ )
138
+ end
139
+
140
+ get '/d' do
141
+ File.mtime(__FILE__).to_s+"<br/>\n"+
142
+ env.map{|k,v| "#{k.inspect} => #{v.inspect} <br/>\n" }.join
143
+ end
144
+ get '/s/:table' do
145
+ @columns = columns(params[:table])
146
+ haml :table_structure
147
+ end
148
+
149
+ post '/t/:table' do
150
+ logger.warn(params.inspect)
151
+ table = quote_table_name(params[:table])
152
+ attrs = params[:attrs]
153
+ if id = attrs.delete('id')
154
+ names = []
155
+ sets = attrs.map{|name, value|
156
+ names << name
157
+ value = value == 'null' ? nil : value[1..-1]
158
+ "#{quote_column_name(name)} = #{quote(value)}"
159
+ }.join(', ')
160
+ sql = "UPDATE #{table} SET #{sets} WHERE id=#{quote(id)}"
161
+ rez = select_all(sql)
162
+ if !rez.error && rez.value > 0
163
+ names_sql = names.map{|n| quote_column_name(n)}.join(', ')
164
+ rez = select_all("SELECT #{names_sql} FROM #{table} WHERE id=#{quote(id)}")
165
+ return rez.rows[0].to_json
166
+ end
167
+ end
168
+ if rez.error
169
+ response.status = 500
170
+ "<pre>#{rez.error.message}</pre>"
171
+ end
172
+ end
173
+
174
+ DEFAULT_QUERY = 'select * from'
175
+ get '/' do
176
+ if params[:query] && params[:query] != DEFAULT_QUERY
177
+ @result = select_all(params[:query])
178
+ else
179
+ set_default_perpage
180
+ end
181
+ @query = params[:query] || DEFAULT_QUERY
182
+ haml :index
183
+ end
184
+
185
+ get '/css.css' do
186
+ sass :css
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,29 @@
1
+ module RailsDbBrowser
2
+ # do mostly same thing as Rack::URLMap
3
+ # but UrlMapper could not work under Rails
4
+ # usage:
5
+ # mounted_app = RailsDbBrowser::URLTruncate.new( RackApplication, '/path')
6
+ # mounted_app = RailsDbBrowser::URLTruncate.new( '/path') do |env| [200, {'Content-type': 'text/plain'}, [env.inspect]] end
7
+ # app = Rack::Builder.app do
8
+ # use RailsDbBrowser::URLTruncate, '/path'
9
+ # run RackApplication
10
+ # end
11
+ class URLTruncate
12
+ def initialize(app_or_path, path = nil, &block)
13
+ @path = path || app_or_path
14
+ @app = block || app_or_path
15
+ end
16
+
17
+ def call(env)
18
+ path, script_name = env.values_at("PATH_INFO", "SCRIPT_NAME")
19
+ if path.start_with?(@path)
20
+ env.merge!('SCRIPT_NAME' => (script_name + @path), 'PATH_INFO' => path[@path.size .. -1] )
21
+ @app.call(env)
22
+ else
23
+ [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]]
24
+ end
25
+ ensure
26
+ env.merge! 'PATH_INFO' => path, 'SCRIPT_NAME' => script_name
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,161 @@
1
+ function fill_id_with_edit_delete($trs) {
2
+ $trs.find('td[data-column=id] span.actions').html(
3
+ ' <a href="javascript:void(0)" class="edit">edit</a>' // <a href="javascript:void(0)" class="delete">delete</a>'
4
+ );
5
+ }
6
+
7
+ $(function(){
8
+ function get_cells(that) {
9
+ var $td = $(that).closest('td');
10
+ var $tr = $td.closest('tr');
11
+ var table = $td.data('table');
12
+ var id = $td.data('id');
13
+ var $tds = $tr.find('td[data-table="'+table+'"]').not($td);
14
+ return {
15
+ $td: $td,
16
+ $tr: $tr,
17
+ table: table,
18
+ $tds: $tds,
19
+ id: id
20
+ }
21
+ }
22
+
23
+ $('td[data-column=id] a.edit').live('click', function(){
24
+ var cells = get_cells(this);
25
+ cells.$tds.each(function(){
26
+ var $textarea = $('<textarea></textarea>');
27
+ var $this = $(this);
28
+ var nil = $this.data('nil');
29
+ $this.data('saved-nil', nil);
30
+ if ( !nil ) {
31
+ var value = $this.attr('data-value');
32
+ $this.attr('data-saved-value', value);
33
+ var lines = value.split(/\n\r|\r\n|\n|\r/);
34
+ var max = 12;
35
+ for(var i=0; i < lines.length; i++) {
36
+ max = Math.max( max, lines[i].length );
37
+ }
38
+ $textarea.attr('rows', Math.max(lines.length + 1, 3));
39
+ $textarea.attr('cols', max + 1);
40
+ } else {
41
+ $textarea.attr('disabled', true);
42
+ $this.attr('data-saved-value', '');
43
+ $textarea.attr('rows', 3);
44
+ $textarea.attr('cols', 13);
45
+ }
46
+ $textarea.val(value);
47
+ var $checkbox = $('<input type="checkbox" name="nil" />');
48
+ $checkbox.attr('checked', !!nil);
49
+ var chbx_id = cells.table + '_' + cells.id + '_' +
50
+ $(this).data('column') + '_nil';
51
+ $checkbox.attr('id', chbx_id);
52
+ var $label = $('<label>NULL:</label>').attr('for', chbx_id);
53
+ $this.html($textarea).append('<br />').
54
+ append($label).append($checkbox);
55
+ })
56
+ cells.$td.find(".actions").html('<a href="javascript:void(0)" class="save">save</a> <a href="javascript:void(0)" class="cancel">cancel</a>');
57
+ });
58
+
59
+ $('td[data-table] input[type=checkbox][name=nil]').live('change', function() {
60
+ var $td = $(this).closest('td');
61
+ var $textarea = $td.find('textarea')
62
+ if ( this.checked ) {
63
+ $td.attr('data-value', $textarea.val());
64
+ $textarea.val('').attr('disabled', true);
65
+ $td.data('nil', true);
66
+ } else {
67
+ $textarea.val($td.attr('data-value')).attr('disabled',false);
68
+ $td.data('nil', false);
69
+ }
70
+ });
71
+
72
+ function fill_td_with_value($td, value, nil) {
73
+ if ( value.match(/^\S+$/) ) {
74
+ $td.text(value);
75
+ $td.removeClass('inspect');
76
+ } else {
77
+ if ( nil ) {
78
+ value = 'nil';
79
+ $td.addClass('inspect');
80
+ } else if ( value.match(/^\s*$/) ){
81
+ value = '"'+value+'"';
82
+ $td.addClass('inspect');
83
+ } else {
84
+ $td.removeClass('inspect');
85
+ }
86
+ var $pre = $('<pre></pre>');
87
+ $pre.text(value);
88
+ $td.html($pre);
89
+ }
90
+ }
91
+
92
+ $('td .actions a.cancel').live('click', function() {
93
+ var cells = get_cells(this);
94
+ cells.$tds.each(function(){
95
+ var $td = $(this);
96
+ $td.attr('data-value', $td.attr('data-saved-value'));
97
+ $td.data('nil', $td.data('saved-nil'));
98
+ var value = $td.attr('data-value');
99
+ var nil = $td.data('nil');
100
+ fill_td_with_value($td, value, nil);
101
+ });
102
+ fill_id_with_edit_delete(cells.$tr);
103
+ });
104
+
105
+ $('td .actions a.save').live('click', function() {
106
+ var cells = get_cells(this);
107
+ var attrs = { "id": cells.id };
108
+ cells.$tds.each(function(){
109
+ var $td = $(this);
110
+ var value = $td.find('input:checkbox[name=nil]').attr('checked') ?
111
+ "null" : '!'+$td.find('textarea').val();
112
+ attrs[$td.attr('data-column')] = value;
113
+ });
114
+ $.ajax({
115
+ url: ROOT+'/t/'+cells.table,
116
+ dataType: 'json',
117
+ data: {attrs: attrs},
118
+ type: 'post',
119
+ success: function(data) {
120
+ cells.$tds.each(function(){
121
+ var $td = $(this);
122
+ if ( $td.data('column') in data ) {
123
+ var value = data[$td.data('column')];
124
+ var nil = value === null;
125
+ value = value ? value : '';
126
+ $td.attr('data-value', value);
127
+ $td.data('nil', nil);
128
+ fill_td_with_value($td, value, nil);
129
+ }
130
+ });
131
+ fill_id_with_edit_delete(cells.$tr);
132
+ },
133
+ error: function(xhr) {
134
+ var $error = $('#error');
135
+ var $error_mask = $('#error_mask');
136
+ $error.find('.content').html(xhr.responseText);
137
+
138
+ var docHeight = $(document).height();
139
+ var winHeight = $(window).height();
140
+ var winWidth = $(window).width();
141
+
142
+ $error_mask.css({ height: winHeight, width: winWidth});
143
+ $error_mask.show();
144
+
145
+ $error.css('display', 'hidden');
146
+ var topleft = ({
147
+ top: Math.max(winHeight - $error.height(), 0)/2,
148
+ left: Math.max(winWidth - $error.width(), 0)/2
149
+ });
150
+ $error.css(topleft);
151
+
152
+ $error.show();
153
+ }
154
+ });
155
+ });
156
+
157
+ $('#error .close, #error_mask').live('click', function() {
158
+ $('#error').hide();
159
+ $('#error_mask').hide();
160
+ });
161
+ });
@@ -0,0 +1,2 @@
1
+ # encoding: utf-8
2
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "init"))
@@ -0,0 +1,5 @@
1
+ %label(for="connection_name") Connection:
2
+ %select.connection_name(name="connection")
3
+ %option(value="") Default
4
+ - connection_names.each do |n|
5
+ %option{:value=>n, :selected=>n == params[:connection]}= n
@@ -0,0 +1,6 @@
1
+ %label(for="rezult_perpage") Per page:
2
+ %select.rezult_perpage(name="perpage")
3
+ %option(value="") No Limits
4
+ - %w{10 25 50 100 250 500 1000}.each do |n|
5
+ %option{:value=>n, :selected=> n == params[:perpage]}= n
6
+
@@ -0,0 +1,58 @@
1
+ body
2
+ font-size: 0.8em
3
+ $hover-background-color: #dfd
4
+ table
5
+ font-size: 100%
6
+ border-spacing: 0
7
+ thead tr
8
+ background-color: lightgrey
9
+ thead tr th
10
+ border-bottom: 1px solid black
11
+ td, th
12
+ border-right: 1px solid black
13
+ &:last-child
14
+ border-right: 0 none
15
+ td.inspect
16
+ background-color: grey
17
+ td.id
18
+ white-space: nowrap
19
+ &.result tbody
20
+ td, td pre
21
+ font-family: monospace
22
+ margin: 0
23
+ td span.actions
24
+ font-family: serif
25
+ font-size: 0.8em
26
+ tr:nth-child(odd)
27
+ background-color: lightcyan
28
+ tr:hover
29
+ background-color: $hover-background-color
30
+ .dbtables
31
+ float: left
32
+ width: 200px
33
+ padding-right: 10px
34
+ h4
35
+ margin: 0.5em 0
36
+ padding: 0
37
+ .list
38
+ height: 20em
39
+ overflow: auto
40
+ .dbtable
41
+ a.q
42
+ display: inline-block
43
+ width: 155px
44
+ .rezult
45
+ clear: left
46
+ #error
47
+ position: absolute
48
+ display: none
49
+ //width: 440px
50
+ z-index: 9999
51
+ border: red 2px solid
52
+ background-color: #FFEEEE
53
+ #error_mask
54
+ position: absolute
55
+ z-index: 9000
56
+ display: none
57
+ left: 0
58
+ top: 0
@@ -0,0 +1,7 @@
1
+ %div.dbtables
2
+ %h4 Tables list:
3
+ .list
4
+ - connection.table_names.each do |tname|
5
+ .dbtable
6
+ %a.q{:href=>table_content_url(tname)}= tname
7
+ %a.s{:href=>table_scheme_url(tname)} (s)
@@ -0,0 +1,8 @@
1
+ %form(method="get")
2
+ = connection_field
3
+ = per_page_field
4
+ %br
5
+ %textarea(name="query" cols="60" rows=10)&= @query
6
+ %br
7
+ %input(type="submit")
8
+ = haml :rezult, :layout => false
@@ -0,0 +1,13 @@
1
+ %html
2
+ %link(rel="stylesheet" type="text/css" href="#{env['SCRIPT_NAME']}/css.css")
3
+ %script(type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.min.js")
4
+ :javascript
5
+ ROOT= '#{url('')}'
6
+ %body
7
+ &= @flash if @flash
8
+ = haml :dbtables, :layout => false
9
+ %div.main= yield
10
+ %div#error
11
+ %a.close(href="javascript: void(0)") Close
12
+ %div.content
13
+ %div#error_mask
@@ -0,0 +1,15 @@
1
+ .pages
2
+ - page = params[:page].to_i
3
+ - nums = (1..5).to_a
4
+ - nums |= ((page-3)..(page+3)).to_a
5
+ - nums |= ((@result.pages-5)..@result.pages).to_a
6
+ - nums = nums.sort.find_all{|i| (1..@result.pages).include?(i)}
7
+ - last_i = 0
8
+ - nums.each do |i|
9
+ - if i > last_i + 1
10
+ &hellip;
11
+ - last_i = i
12
+ - unless i == page
13
+ %a{:href=> merge_params("page" => i)}= i
14
+ - else
15
+ = i
@@ -0,0 +1,75 @@
1
+ %script{:src => url('/result.js')}
2
+ .rezult
3
+ - if @result
4
+ - if @result.rows?
5
+ - if @result.count > 0
6
+ - fields = @result.fields
7
+ %strong= "Total: #{@result.count}"
8
+ - if @result.pages > 1
9
+ = haml :pages, :layout => false
10
+ %table.result
11
+ %thead
12
+ - if fields.any?{|f| f.include?('.')}
13
+ - tables = extract_tables(fields)
14
+ %tr
15
+ - tables.each do |t|
16
+ %th{:colspan => t.columns.size}&= t.table
17
+ %tr
18
+ - tables.map(&:columns).flatten.each do |column|
19
+ %th&= column
20
+ - else
21
+ %tr
22
+ - fields.each do |field|
23
+ %th&= field
24
+ %tbody
25
+ - @result.rows.each do |row|
26
+ - row_ids = {}
27
+ %tr
28
+ - fields.each do |field|
29
+ - value = row[field]
30
+ - if field =~ /([\w\.]+)\.(\w+)/
31
+ - table, column = $1, $2
32
+ - if column == 'id'
33
+ - row_ids[table] = value
34
+ - is_id = true
35
+ - else
36
+ - is_id = false
37
+ - data = { :table => table, :column => column, :id => row_ids[table], :value => value, :nil => value.nil? }
38
+ - else
39
+ - data = nil
40
+ - if value == nil
41
+ - value = 'nil'
42
+ - empty = true
43
+ - elsif value =~ /^\s*$/
44
+ - value = '"'+value+'"'
45
+ - empty = true
46
+ %td{:class => [(empty && 'inspect'), data && data[:column] == 'id' && 'id'], :data => data}
47
+ - if value !~ /^\S+$/
48
+ %pre= preserve(html_escape(value))
49
+ - elsif is_id
50
+ %span.actions &nbsp;
51
+ = value
52
+ - else
53
+ = value
54
+ - if @result.pages > 1
55
+ = haml :pages, :layout => false
56
+ :javascript
57
+ fill_id_with_edit_delete($('table.result'));
58
+ - else
59
+ Has no returned rows
60
+ - elsif @result.value?
61
+ Rezult:
62
+ &= @result.value.inspect
63
+ - elsif @result.error?
64
+ Error:
65
+ &= @result.error.class.name
66
+ %br/
67
+ Message:
68
+ &= @result.error.message
69
+ %br/
70
+ Traceback:
71
+ %pre
72
+ - @result.error.backtrace.each do |l|
73
+ &= l.sub(Rails.root, '')
74
+ - else
75
+ Has no result
@@ -0,0 +1,21 @@
1
+ %h1&= "Table #{params[:table]}"
2
+ %a{:href=>url('')} goto queries
3
+ %a{:href=>table_content_url(params[:table])} goto table
4
+ %form(method="get")
5
+ = connection_field
6
+ %input{:type=>"hidden", :name=>"perpage", :value=>params[:perpage]}
7
+ %input(type="submit")
8
+ %table.columns
9
+ %thead
10
+ %tr
11
+ %th Name
12
+ %th Type
13
+ %th Default
14
+ %th Not Null
15
+ %tbody
16
+ - @columns.each do |col|
17
+ %tr
18
+ %td= col.name
19
+ %td= col.type
20
+ %td&= col.default.inspect
21
+ %td= col.null
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_db_browser
3
+ version: !ruby/object:Gem::Version
4
+ hash: 13
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 9
10
+ version: 0.0.9
11
+ platform: ruby
12
+ authors:
13
+ - Sokolov Yura aka funny_falcon
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-03-04 00:00:00 +03:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: sinatra
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: haml
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ description: Simple sinatra Rack application that allowes to run sql queries from a web page. Usable for administrative tasks.
50
+ email: funny.falcon@gmail.com
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files:
56
+ - README
57
+ files:
58
+ - .gitignore
59
+ - MIT-LICENSE
60
+ - README
61
+ - Rakefile
62
+ - VERSION
63
+ - init.rb
64
+ - lib/rails_db_browser.rb
65
+ - lib/rails_db_browser/connection_keeper.rb
66
+ - lib/rails_db_browser/db_browser.rb
67
+ - lib/rails_db_browser/url_truncate.rb
68
+ - public/result.js
69
+ - rails/init.rb
70
+ - views/_connection_field.haml
71
+ - views/_per_page_field.haml
72
+ - views/css.sass
73
+ - views/dbtables.haml
74
+ - views/index.haml
75
+ - views/layout.haml
76
+ - views/pages.haml
77
+ - views/rezult.haml
78
+ - views/table_structure.haml
79
+ has_rdoc: true
80
+ homepage: http://github.com/funny-falcon/rails_db_browser
81
+ licenses: []
82
+
83
+ post_install_message:
84
+ rdoc_options:
85
+ - --charset=UTF-8
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ hash: 3
94
+ segments:
95
+ - 0
96
+ version: "0"
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ hash: 3
103
+ segments:
104
+ - 0
105
+ version: "0"
106
+ requirements: []
107
+
108
+ rubyforge_project:
109
+ rubygems_version: 1.5.2
110
+ signing_key:
111
+ specification_version: 3
112
+ summary: Simple database browser for Rails application backed by ActiveRecord
113
+ test_files: []
114
+