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.
- 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
|
+
|