rails_db_browser 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/MIT-LICENSE +20 -0
- data/README +60 -0
- data/Rakefile +28 -0
- data/VERSION +1 -0
- data/init.rb +2 -0
- data/lib/rails_db_browser.rb +17 -0
- data/lib/rails_db_browser/connection_keeper.rb +169 -0
- data/lib/rails_db_browser/db_browser.rb +189 -0
- data/lib/rails_db_browser/url_truncate.rb +29 -0
- data/public/result.js +161 -0
- data/rails/init.rb +2 -0
- data/views/_connection_field.haml +5 -0
- data/views/_per_page_field.haml +6 -0
- data/views/css.sass +58 -0
- data/views/dbtables.haml +7 -0
- data/views/index.haml +8 -0
- data/views/layout.haml +13 -0
- data/views/pages.haml +15 -0
- data/views/rezult.haml +75 -0
- data/views/table_structure.haml +21 -0
- metadata +114 -0
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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,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
|
data/public/result.js
ADDED
@@ -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
|
+
});
|
data/rails/init.rb
ADDED
data/views/css.sass
ADDED
@@ -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
|
data/views/dbtables.haml
ADDED
data/views/index.haml
ADDED
data/views/layout.haml
ADDED
@@ -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
|
data/views/pages.haml
ADDED
@@ -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
|
+
…
|
11
|
+
- last_i = i
|
12
|
+
- unless i == page
|
13
|
+
%a{:href=> merge_params("page" => i)}= i
|
14
|
+
- else
|
15
|
+
= i
|
data/views/rezult.haml
ADDED
@@ -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
|
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
|
+
|