rails_db_browser 0.0.9

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