sqlui 0.1.46 → 0.1.48
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/app/database_config.rb +45 -6
- data/app/server.rb +51 -32
- data/app/sql_parser.rb +74 -10
- data/app/sqlui.rb +1 -0
- data/app/views/error.erb +21 -0
- data/client/resources/favicon.svg +1 -0
- data/client/resources/sqlui.css +26 -3
- data/client/resources/sqlui.html +1 -0
- data/client/resources/sqlui.js +243 -205
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ddbafc23478bb1d27023d516dda3ef412306c68e3084aa9c7e186c7372ff18ad
|
4
|
+
data.tar.gz: 5638fa4d6a7e30ecce0d89505953e2fc2d9f18e19e928c41f7c1bde4a986aba9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c99d375bf14a2f892b1a7c3e85ec576ebdc749e432e56f7e993c8437c89e8a19af96189a501b7a80e08e3b6f5743eeecd57dbfa94b056570ba1da0274d163121
|
7
|
+
data.tar.gz: 3fab12e8be52afbd23f317b16c48eed4d095f293f4bab02a9c4c515ea91d501fd027fa71b1f008831d86fa00af38fb4376ba6c3f891ba5a4c7aa7f0b39690e2c
|
data/.version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.48
|
data/app/database_config.rb
CHANGED
@@ -8,7 +8,7 @@ require_relative 'args'
|
|
8
8
|
|
9
9
|
# Config for a single database.
|
10
10
|
class DatabaseConfig
|
11
|
-
attr_reader :display_name, :description, :url_path, :joins, :saved_path, :
|
11
|
+
attr_reader :display_name, :description, :url_path, :joins, :saved_path, :tables, :columns, :client_params
|
12
12
|
|
13
13
|
def initialize(hash)
|
14
14
|
@display_name = Args.fetch_non_empty_string(hash, :display_name).strip
|
@@ -27,12 +27,51 @@ class DatabaseConfig
|
|
27
27
|
|
28
28
|
raise ArgumentError, "invalid join #{join.to_json}"
|
29
29
|
end
|
30
|
-
@
|
31
|
-
@
|
32
|
-
|
30
|
+
@tables = Args.fetch_optional_hash(hash, :tables) || {}
|
31
|
+
@tables.each do |table, table_config|
|
32
|
+
unless table_config.is_a?(Hash)
|
33
|
+
raise ArgumentError, "invalid table config for #{table} (#{table_config}), expected hash"
|
34
|
+
end
|
35
|
+
|
36
|
+
table_alias = table_config[:alias]
|
37
|
+
if table_alias && !table_alias.is_a?(String)
|
38
|
+
raise ArgumentError, "invalid table alias for #{table} (#{table_alias}), expected string"
|
39
|
+
end
|
40
|
+
|
41
|
+
table_boost = table_config[:boost]
|
42
|
+
if table_boost && !table_boost.is_a?(Integer)
|
43
|
+
raise ArgumentError, "invalid table boost for #{table} (#{table_boost}), expected int"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
@columns = Args.fetch_optional_hash(hash, :columns) || {}
|
47
|
+
@columns.each do |column, column_config|
|
48
|
+
unless column_config.is_a?(Hash)
|
49
|
+
raise ArgumentError, "invalid column config for #{column} (#{column_config}), expected hash"
|
50
|
+
end
|
51
|
+
|
52
|
+
links = column_config[:links]
|
53
|
+
raise ArgumentError, "invalid links for #{column} (#{links}), expected array" if links && !links.is_a?(Array)
|
54
|
+
|
55
|
+
links.each do |link_config|
|
56
|
+
unless link_config.is_a?(Hash)
|
57
|
+
raise ArgumentError, "invalid link config for #{column} (#{link_config}), expected hash"
|
58
|
+
end
|
59
|
+
|
60
|
+
unless link_config[:short_name].is_a?(String)
|
61
|
+
raise ArgumentError,
|
62
|
+
"invalid link short_name for #{column} link (#{link_config[:short_name]}), expected string"
|
63
|
+
end
|
64
|
+
unless link_config[:long_name].is_a?(String)
|
65
|
+
raise ArgumentError, "invalid link long_name for #{column} link (#{link_config[:long_name]}), expected string"
|
66
|
+
end
|
67
|
+
unless link_config[:template].is_a?(String)
|
68
|
+
raise ArgumentError, "invalid template for #{column} link (#{link_config[:template]}), expected string"
|
69
|
+
end
|
70
|
+
end
|
33
71
|
end
|
34
|
-
|
35
|
-
if
|
72
|
+
aliases = @tables.map { |_table, table_config| table_config[:alias] }.compact
|
73
|
+
if aliases.to_set.size < aliases.size
|
74
|
+
duplicate_aliases = aliases.reject { |a| aliases.count(a) == 1 }.to_set
|
36
75
|
raise ArgumentError, "duplicate table aliases: #{duplicate_aliases.join(', ')}"
|
37
76
|
end
|
38
77
|
|
data/app/server.rb
CHANGED
@@ -6,7 +6,6 @@ require 'erb'
|
|
6
6
|
require 'json'
|
7
7
|
require 'sinatra/base'
|
8
8
|
require 'uri'
|
9
|
-
require 'webrick/log'
|
10
9
|
require_relative 'database_metadata'
|
11
10
|
require_relative 'mysql_types'
|
12
11
|
require_relative 'sql_parser'
|
@@ -14,6 +13,10 @@ require_relative 'sqlui'
|
|
14
13
|
|
15
14
|
# SQLUI Sinatra server.
|
16
15
|
class Server < Sinatra::Base
|
16
|
+
def self.logger
|
17
|
+
@logger ||= WEBrick::Log.new
|
18
|
+
end
|
19
|
+
|
17
20
|
def self.init_and_run(config, resources_dir)
|
18
21
|
Mysql2::Client.default_query_options[:as] = :array
|
19
22
|
Mysql2::Client.default_query_options[:cast_booleans] = true
|
@@ -27,8 +30,6 @@ class Server < Sinatra::Base
|
|
27
30
|
set :raise_errors, false
|
28
31
|
set :show_exceptions, false
|
29
32
|
|
30
|
-
logger = WEBrick::Log.new
|
31
|
-
|
32
33
|
get '/-/health' do
|
33
34
|
status 200
|
34
35
|
body 'OK'
|
@@ -38,6 +39,10 @@ class Server < Sinatra::Base
|
|
38
39
|
redirect config.list_url_path, 301
|
39
40
|
end
|
40
41
|
|
42
|
+
get '/favicon.svg' do
|
43
|
+
send_file File.join(resources_dir, 'favicon.svg')
|
44
|
+
end
|
45
|
+
|
41
46
|
get "#{config.list_url_path}/?" do
|
42
47
|
erb :databases, locals: { config: config }
|
43
48
|
end
|
@@ -67,7 +72,8 @@ class Server < Sinatra::Base
|
|
67
72
|
server: "#{config.name} - #{database.display_name}",
|
68
73
|
list_url_path: config.list_url_path,
|
69
74
|
schemas: DatabaseMetadata.lookup(client, database),
|
70
|
-
|
75
|
+
tables: database.tables,
|
76
|
+
columns: database.columns,
|
71
77
|
joins: database.joins,
|
72
78
|
saved: Dir.glob("#{database.saved_path}/*.sql").to_h do |path|
|
73
79
|
contents = File.read(path)
|
@@ -99,35 +105,45 @@ class Server < Sinatra::Base
|
|
99
105
|
variables = params[:variables] || {}
|
100
106
|
sql = find_selected_query(params[:sql], params[:selection])
|
101
107
|
|
102
|
-
result = database.with_client do |client|
|
103
|
-
query_result = execute_query(client, variables, sql)
|
104
|
-
# NOTE: the call to result.field_types must go before other interaction with the result. Otherwise you will
|
105
|
-
# get a seg fault. Seems to be a bug in Mysql2.
|
106
|
-
# TODO: stream this and render results on the client as they are returned?
|
107
|
-
{
|
108
|
-
columns: query_result.fields,
|
109
|
-
column_types: MysqlTypes.map_to_google_charts_types(query_result.field_types),
|
110
|
-
total_rows: query_result.size,
|
111
|
-
rows: (query_result.to_a || []).take(Sqlui::MAX_ROWS)
|
112
|
-
}
|
113
|
-
end
|
114
|
-
|
115
|
-
result[:selection] = params[:selection]
|
116
|
-
result[:query] = params[:sql]
|
117
|
-
|
118
108
|
status 200
|
119
109
|
headers 'Content-Type' => 'application/json; charset=utf-8'
|
120
|
-
|
110
|
+
|
111
|
+
database.with_client do |client|
|
112
|
+
query_result = execute_query(client, variables, sql)
|
113
|
+
stream do |out|
|
114
|
+
json = <<~RES.chomp
|
115
|
+
{
|
116
|
+
"columns": #{query_result.fields.to_json},
|
117
|
+
"column_types": #{MysqlTypes.map_to_google_charts_types(query_result.field_types).to_json},
|
118
|
+
"total_rows": #{query_result.size.to_json},
|
119
|
+
"selection": #{params[:selection].to_json},
|
120
|
+
"query": #{params[:sql].to_json},
|
121
|
+
"rows": [
|
122
|
+
RES
|
123
|
+
out << json
|
124
|
+
bytes = json.bytesize
|
125
|
+
query_result.each_with_index do |row, i|
|
126
|
+
json = "#{i.zero? ? '' : ','}\n #{row.to_json}"
|
127
|
+
bytes += json.bytesize
|
128
|
+
break if i == Sqlui::MAX_ROWS || bytes > Sqlui::MAX_BYTES
|
129
|
+
|
130
|
+
out << json
|
131
|
+
end
|
132
|
+
out << <<~RES
|
133
|
+
|
134
|
+
]
|
135
|
+
}
|
136
|
+
RES
|
137
|
+
end
|
138
|
+
end
|
121
139
|
end
|
122
140
|
|
123
141
|
get "#{database.url_path}/download_csv" do
|
124
142
|
break client_error('missing sql') unless params[:sql]
|
125
143
|
|
126
144
|
sql = Base64.decode64(params[:sql]).force_encoding('UTF-8')
|
127
|
-
logger.info "sql: #{sql}"
|
128
145
|
variables = params.map { |k, v| k[0] == '_' ? [k, v] : nil }.compact.to_h
|
129
146
|
sql = find_selected_query(sql, params[:selection])
|
130
|
-
logger.info "sql: #{sql}"
|
131
147
|
|
132
148
|
content_type 'application/csv; charset=utf-8'
|
133
149
|
attachment 'result.csv'
|
@@ -152,15 +168,18 @@ class Server < Sinatra::Base
|
|
152
168
|
end
|
153
169
|
end
|
154
170
|
|
155
|
-
error do
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
171
|
+
error 400..510 do
|
172
|
+
exception = env['sinatra.error']
|
173
|
+
stacktrace = exception&.full_message(highlight: false)
|
174
|
+
if request.env['HTTP_ACCEPT'] == 'application/json'
|
175
|
+
headers 'Content-Type' => 'application/json; charset=utf-8'
|
176
|
+
message = exception&.message&.lines&.first&.strip || 'unexpected error'
|
177
|
+
json = { error: message, stacktrace: stacktrace }.compact.to_json
|
178
|
+
body json
|
179
|
+
else
|
180
|
+
message = "#{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}"
|
181
|
+
erb :error, locals: { title: "SQLUI #{message}", message: message, stacktrace: stacktrace }
|
182
|
+
end
|
164
183
|
end
|
165
184
|
|
166
185
|
run!
|
data/app/sql_parser.rb
CHANGED
@@ -1,19 +1,83 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'strscan'
|
4
|
+
|
3
5
|
# Used to parse strings containing one or more SQL statements.
|
4
6
|
class SqlParser
|
5
7
|
def self.find_statement_at_cursor(sql, cursor)
|
6
|
-
|
7
|
-
|
8
|
-
parts_with_ranges = []
|
9
|
-
sql.scan(/[^;]*;[ \n]*/) { |part| parts_with_ranges << [part, 0, part.size] }
|
10
|
-
parts_with_ranges.inject(0) do |pos, current|
|
11
|
-
current[1] += pos
|
12
|
-
current[2] += pos
|
13
|
-
end
|
8
|
+
parts_with_ranges = parse(sql)
|
14
9
|
part_with_range = parts_with_ranges.find do |current|
|
15
|
-
cursor >= current[1] && cursor
|
10
|
+
cursor >= current[1] && cursor <= current[2]
|
16
11
|
end || parts_with_ranges[-1]
|
17
|
-
part_with_range[0]
|
12
|
+
part_with_range[0]
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.parse(sql)
|
16
|
+
scanner = StringScanner.new(sql)
|
17
|
+
statements = []
|
18
|
+
single_quoted = false
|
19
|
+
double_quoted = false
|
20
|
+
single_commented = false
|
21
|
+
multi_commented = false
|
22
|
+
escaped = false
|
23
|
+
current = ''
|
24
|
+
start = 0
|
25
|
+
until scanner.eos?
|
26
|
+
current ||= ''
|
27
|
+
char = scanner.getch
|
28
|
+
current += char
|
29
|
+
|
30
|
+
if (single_quoted || double_quoted) && !escaped && char == "'" && scanner.peek(1) == "'"
|
31
|
+
current += scanner.getch
|
32
|
+
elsif escaped
|
33
|
+
escaped = false
|
34
|
+
elsif !single_quoted && !double_quoted && !escaped && !single_commented && char == '/' && scanner.peek(1) == '*'
|
35
|
+
current += scanner.getch
|
36
|
+
multi_commented = true
|
37
|
+
elsif multi_commented && char == '*' && scanner.peek(1) == '/'
|
38
|
+
multi_commented = false
|
39
|
+
elsif multi_commented
|
40
|
+
next
|
41
|
+
elsif !single_quoted && !double_quoted && !escaped && !multi_commented &&
|
42
|
+
char == '-' && scanner.peek(1).match?(/-[ \n\t]/)
|
43
|
+
current += scanner.getch
|
44
|
+
single_commented = true
|
45
|
+
elsif !single_quoted && !double_quoted && !escaped && !multi_commented && char == '#'
|
46
|
+
single_commented = true
|
47
|
+
elsif single_commented && char == "\n"
|
48
|
+
single_commented = false
|
49
|
+
elsif single_commented
|
50
|
+
single_quoted = false if char == "\n"
|
51
|
+
elsif char == '\\'
|
52
|
+
escaped = true
|
53
|
+
elsif char == "'"
|
54
|
+
single_quoted = !single_quoted unless double_quoted
|
55
|
+
elsif char == '"'
|
56
|
+
double_quoted = !double_quoted unless single_quoted
|
57
|
+
elsif char == ';' && !single_quoted && !double_quoted
|
58
|
+
# Include trailing whitespace if it runs to end of line
|
59
|
+
current += scanner.scan(/[ \t]*(?=\n)/) || ''
|
60
|
+
|
61
|
+
# Include an optional trailing single line comma
|
62
|
+
current += scanner.scan(/[ \t]*--[ \t][^\n]*/) || ''
|
63
|
+
current += scanner.scan(/[ \t]*#[^\n]*/) || ''
|
64
|
+
|
65
|
+
# Include any following blank lines, careful to avoid the final newline in case it is the start of a new query
|
66
|
+
while (more = scanner.scan(/\n[ \t]*(?=\n)/))
|
67
|
+
current += more
|
68
|
+
end
|
69
|
+
|
70
|
+
# Include remaining whitespace if that's all that's left
|
71
|
+
if scanner.rest_size == scanner.match?(/[ \t\n]*/)
|
72
|
+
current += scanner.rest
|
73
|
+
scanner.terminate
|
74
|
+
end
|
75
|
+
statements << [current, start, scanner.pos]
|
76
|
+
start = scanner.pos
|
77
|
+
current = nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
statements << [current, start, scanner.pos] unless current.nil?
|
81
|
+
statements
|
18
82
|
end
|
19
83
|
end
|
data/app/sqlui.rb
CHANGED
data/app/views/error.erb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
<html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<title><%= title %></title>
|
6
|
+
<link rel="icon" type="image/x-icon" href="/favicon.svg">
|
7
|
+
</head>
|
8
|
+
<body style="font-family: monospace; font-size: 16px;">
|
9
|
+
|
10
|
+
<% if defined?(message) && message %>
|
11
|
+
<p><%= message %></p>
|
12
|
+
<% end %>
|
13
|
+
|
14
|
+
<% if defined?(stacktrace) && stacktrace %>
|
15
|
+
<pre>
|
16
|
+
<%= stacktrace %>
|
17
|
+
</pre>
|
18
|
+
<% end %>
|
19
|
+
|
20
|
+
</body>
|
21
|
+
</html>
|
@@ -0,0 +1 @@
|
|
1
|
+
<svg version="1.1" viewBox="0.0 0.0 32.0 32.0" fill="none" stroke="none" stroke-linecap="square" stroke-miterlimit="10" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><clipPath id="p.0"><path d="m0 0l32.0 0l0 32.0l-32.0 0l0 -32.0z" clip-rule="nonzero"/></clipPath><g clip-path="url(#p.0)"><path fill="#000000" fill-opacity="0.0" d="m0 0l32.0 0l0 32.0l-32.0 0z" fill-rule="evenodd"/><path fill="#6d9eeb" d="m1.3070866 20.064245l0 0c0 1.6235943 6.5782413 2.9397774 14.692913 2.9397774c8.114672 0 14.692913 -1.3161831 14.692913 -2.9397774l0 8.073204c0 1.6235943 -6.5782413 2.9397774 -14.692913 2.9397774c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397774z" fill-rule="evenodd"/><path fill="#a7c4f3" d="m1.3070866 20.064245l0 0c0 -1.6235924 6.5782413 -2.9397755 14.692913 -2.9397755c8.114672 0 14.692913 1.3161831 14.692913 2.9397755l0 0c0 1.6235943 -6.5782413 2.9397774 -14.692913 2.9397774c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397774z" fill-rule="evenodd"/><path fill="#000000" fill-opacity="0.0" d="m30.692913 20.064245l0 0c0 1.6235943 -6.5782413 2.9397774 -14.692913 2.9397774c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397774l0 0c0 -1.6235924 6.5782413 -2.9397755 14.692913 -2.9397755c8.114672 0 14.692913 1.3161831 14.692913 2.9397755l0 8.073204c0 1.6235943 -6.5782413 2.9397774 -14.692913 2.9397774c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397774l0 -8.073204" fill-rule="evenodd"/><path stroke="#351c75" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m30.692913 20.064245l0 0c0 1.6235943 -6.5782413 2.9397774 -14.692913 2.9397774c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397774l0 0c0 -1.6235924 6.5782413 -2.9397755 14.692913 -2.9397755c8.114672 0 14.692913 1.3161831 14.692913 2.9397755l0 8.073204c0 1.6235943 -6.5782413 2.9397774 -14.692913 2.9397774c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397774l0 -8.073204" fill-rule="evenodd"/><path fill="#93c47d" d="m1.3070866 12.012567l0 0c0 1.6235943 6.5782413 2.9397764 14.692913 2.9397764c8.114672 0 14.692913 -1.3161821 14.692913 -2.9397764l0 8.073205c0 1.6235924 -6.5782413 2.9397755 -14.692913 2.9397755c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397755z" fill-rule="evenodd"/><path fill="#bedbb1" d="m1.3070866 12.012567l0 0c0 -1.6235933 6.5782413 -2.9397755 14.692913 -2.9397755c8.114672 0 14.692913 1.3161821 14.692913 2.9397755l0 0c0 1.6235943 -6.5782413 2.9397764 -14.692913 2.9397764c-8.114672 0 -14.692913 -1.3161821 -14.692913 -2.9397764z" fill-rule="evenodd"/><path fill="#000000" fill-opacity="0.0" d="m30.692913 12.012567l0 0c0 1.6235943 -6.5782413 2.9397764 -14.692913 2.9397764c-8.114672 0 -14.692913 -1.3161821 -14.692913 -2.9397764l0 0c0 -1.6235933 6.5782413 -2.9397755 14.692913 -2.9397755c8.114672 0 14.692913 1.3161821 14.692913 2.9397755l0 8.073205c0 1.6235924 -6.5782413 2.9397755 -14.692913 2.9397755c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397755l0 -8.073205" fill-rule="evenodd"/><path stroke="#351c75" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m30.692913 12.012567l0 0c0 1.6235943 -6.5782413 2.9397764 -14.692913 2.9397764c-8.114672 0 -14.692913 -1.3161821 -14.692913 -2.9397764l0 0c0 -1.6235933 6.5782413 -2.9397755 14.692913 -2.9397755c8.114672 0 14.692913 1.3161821 14.692913 2.9397755l0 8.073205c0 1.6235924 -6.5782413 2.9397755 -14.692913 2.9397755c-8.114672 0 -14.692913 -1.3161831 -14.692913 -2.9397755l0 -8.073205" fill-rule="evenodd"/><path fill="#f4cccc" d="m1.3070866 3.862348l0 0c0 1.6235933 6.5782413 2.939776 14.692913 2.939776c8.114672 0 14.692913 -1.3161826 14.692913 -2.939776l0 8.073204c0 1.6235933 -6.5782413 2.9397755 -14.692913 2.9397755c-8.114672 0 -14.692913 -1.3161821 -14.692913 -2.9397755z" fill-rule="evenodd"/><path fill="#f8e0e0" d="m1.3070866 3.862348l0 0c0 -1.6235933 6.5782413 -2.939776 14.692913 -2.939776c8.114672 0 14.692913 1.3161826 14.692913 2.939776l0 0c0 1.6235933 -6.5782413 2.939776 -14.692913 2.939776c-8.114672 0 -14.692913 -1.3161826 -14.692913 -2.939776z" fill-rule="evenodd"/><path fill="#000000" fill-opacity="0.0" d="m30.692913 3.862348l0 0c0 1.6235933 -6.5782413 2.939776 -14.692913 2.939776c-8.114672 0 -14.692913 -1.3161826 -14.692913 -2.939776l0 0c0 -1.6235933 6.5782413 -2.939776 14.692913 -2.939776c8.114672 0 14.692913 1.3161826 14.692913 2.939776l0 8.073204c0 1.6235933 -6.5782413 2.9397755 -14.692913 2.9397755c-8.114672 0 -14.692913 -1.3161821 -14.692913 -2.9397755l0 -8.073204" fill-rule="evenodd"/><path stroke="#351c75" stroke-width="1.0" stroke-linejoin="round" stroke-linecap="butt" d="m30.692913 3.862348l0 0c0 1.6235933 -6.5782413 2.939776 -14.692913 2.939776c-8.114672 0 -14.692913 -1.3161826 -14.692913 -2.939776l0 0c0 -1.6235933 6.5782413 -2.939776 14.692913 -2.939776c8.114672 0 14.692913 1.3161826 14.692913 2.939776l0 8.073204c0 1.6235933 -6.5782413 2.9397755 -14.692913 2.9397755c-8.114672 0 -14.692913 -1.3161821 -14.692913 -2.9397755l0 -8.073204" fill-rule="evenodd"/></g></svg>
|
data/client/resources/sqlui.css
CHANGED
@@ -158,7 +158,7 @@ p {
|
|
158
158
|
border-left: 1px solid #888;
|
159
159
|
border-right: 1px solid #888;
|
160
160
|
border-bottom: 1px solid #888;
|
161
|
-
z-index:
|
161
|
+
z-index: 2;
|
162
162
|
flex-direction: column;
|
163
163
|
left: 0;
|
164
164
|
right: 0;
|
@@ -246,6 +246,29 @@ p {
|
|
246
246
|
flex-direction: column;
|
247
247
|
}
|
248
248
|
|
249
|
+
#result-table tbody tr td abbr a {
|
250
|
+
color: #555;
|
251
|
+
cursor: pointer;
|
252
|
+
text-decoration: none;
|
253
|
+
margin: 0;
|
254
|
+
padding: 0 3px;
|
255
|
+
border: 1px dotted #555;
|
256
|
+
font-size: 12px;
|
257
|
+
display: inline-block;
|
258
|
+
}
|
259
|
+
|
260
|
+
#result-table tbody tr td abbr {
|
261
|
+
margin: 0 0 0 5px;
|
262
|
+
text-decoration: none;
|
263
|
+
display: inline-block;
|
264
|
+
position: relative;
|
265
|
+
top: -2px;
|
266
|
+
}
|
267
|
+
|
268
|
+
#result-table tbody tr td abbr:first-child {
|
269
|
+
margin: 0 0 0 10px;
|
270
|
+
}
|
271
|
+
|
249
272
|
.fetch-sql-box {
|
250
273
|
justify-content: center;
|
251
274
|
align-items: center;
|
@@ -271,7 +294,7 @@ table td:last-child, table th:last-child {
|
|
271
294
|
}
|
272
295
|
|
273
296
|
td, th {
|
274
|
-
padding: 5px
|
297
|
+
padding: 5px 10px;
|
275
298
|
font-weight: normal;
|
276
299
|
white-space: nowrap;
|
277
300
|
max-width: 500px;
|
@@ -300,7 +323,7 @@ thead {
|
|
300
323
|
position: -webkit-sticky;
|
301
324
|
position: sticky;
|
302
325
|
top: 0;
|
303
|
-
z-index:
|
326
|
+
z-index: 1;
|
304
327
|
table-layout: fixed;
|
305
328
|
}
|
306
329
|
|
data/client/resources/sqlui.html
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
<head>
|
3
3
|
<meta charset="utf-8">
|
4
4
|
<title>SQLUI</title>
|
5
|
+
<link rel="icon" type="image/x-icon" href="/favicon.svg">
|
5
6
|
<script src="sqlui.js"></script>
|
6
7
|
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
|
7
8
|
<link rel="stylesheet" href="sqlui.css">
|
data/client/resources/sqlui.js
CHANGED
@@ -21666,6 +21666,57 @@
|
|
21666
21666
|
}
|
21667
21667
|
});
|
21668
21668
|
|
21669
|
+
function base64Encode (str) {
|
21670
|
+
// https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
|
21671
|
+
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
|
21672
|
+
function toSolidBytes (match, p1) {
|
21673
|
+
return String.fromCharCode(Number(`0x${p1}`))
|
21674
|
+
}))
|
21675
|
+
}
|
21676
|
+
|
21677
|
+
function copyTextToClipboard (text) {
|
21678
|
+
const type = 'text/plain';
|
21679
|
+
const blob = new Blob([text], { type });
|
21680
|
+
navigator.clipboard.write([new window.ClipboardItem({ [type]: blob })]);
|
21681
|
+
}
|
21682
|
+
|
21683
|
+
function toCsv (columns, rows) {
|
21684
|
+
let text = columns.map((header) => {
|
21685
|
+
if (typeof header === 'string' && (header.includes('"') || header.includes(','))) {
|
21686
|
+
return `"${header.replaceAll('"', '""')}"`
|
21687
|
+
} else {
|
21688
|
+
return header
|
21689
|
+
}
|
21690
|
+
}).join(',') + '\n';
|
21691
|
+
text += rows.map((row) => {
|
21692
|
+
return row.map((cell) => {
|
21693
|
+
if (typeof cell === 'string' && (cell.includes('"') || cell.includes(','))) {
|
21694
|
+
return `"${cell.replaceAll('"', '""')}"`
|
21695
|
+
} else {
|
21696
|
+
return cell
|
21697
|
+
}
|
21698
|
+
}).join(',')
|
21699
|
+
}).join('\n');
|
21700
|
+
|
21701
|
+
return text
|
21702
|
+
}
|
21703
|
+
|
21704
|
+
function toTsv (columns, rows) {
|
21705
|
+
if (columns.find((cell) => cell === '\t')) {
|
21706
|
+
throw new Error('TSV input may not contain a tab character.')
|
21707
|
+
}
|
21708
|
+
let text = columns.join('\t') + '\n';
|
21709
|
+
|
21710
|
+
text += rows.map((row) => {
|
21711
|
+
if (row.find((cell) => cell === '\t')) {
|
21712
|
+
throw new Error('TSV input may not contain a tab character.')
|
21713
|
+
}
|
21714
|
+
return row.join('\t')
|
21715
|
+
}).join('\n');
|
21716
|
+
|
21717
|
+
return text
|
21718
|
+
}
|
21719
|
+
|
21669
21720
|
/// A parse stack. These are used internally by the parser to track
|
21670
21721
|
/// parsing progress. They also provide some properties and methods
|
21671
21722
|
/// that external code such as a tokenizer can use to get information
|
@@ -23860,131 +23911,7 @@
|
|
23860
23911
|
builtin: MySQLBuiltin
|
23861
23912
|
});
|
23862
23913
|
|
23863
|
-
|
23864
|
-
|
23865
|
-
function unquoteSqlId (identifier) {
|
23866
|
-
const match = identifier.match(/^`(.*)`$/);
|
23867
|
-
return match ? match[1] : identifier
|
23868
|
-
}
|
23869
|
-
|
23870
|
-
function base64Encode (str) {
|
23871
|
-
// https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
|
23872
|
-
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
|
23873
|
-
function toSolidBytes (match, p1) {
|
23874
|
-
return String.fromCharCode(Number(`0x${p1}`))
|
23875
|
-
}))
|
23876
|
-
}
|
23877
|
-
|
23878
|
-
function getSqlFromUrl (url) {
|
23879
|
-
const params = url.searchParams;
|
23880
|
-
if (params.has('file') && params.has('sql')) {
|
23881
|
-
// TODO: show an error.
|
23882
|
-
throw new Error('You can only specify a file or sql param, not both.')
|
23883
|
-
}
|
23884
|
-
if (params.has('sql')) {
|
23885
|
-
return params.get('sql')
|
23886
|
-
} else if (params.has('file')) {
|
23887
|
-
const file = params.get('file');
|
23888
|
-
const fileDetails = window.metadata.saved[file];
|
23889
|
-
if (!fileDetails) throw new Error(`no such file: ${file}`)
|
23890
|
-
return fileDetails.contents
|
23891
|
-
}
|
23892
|
-
throw new Error('You must specify a file or sql param')
|
23893
|
-
}
|
23894
|
-
|
23895
|
-
function init (parent, onSubmit, onShiftSubmit) {
|
23896
|
-
addClickListener(document.getElementById('query-tab-button'), (event) => selectTab(event, 'query'));
|
23897
|
-
addClickListener(document.getElementById('saved-tab-button'), (event) => selectTab(event, 'saved'));
|
23898
|
-
addClickListener(document.getElementById('structure-tab-button'), (event) => selectTab(event, 'structure'));
|
23899
|
-
addClickListener(document.getElementById('graph-tab-button'), (event) => selectTab(event, 'graph'));
|
23900
|
-
addClickListener(document.getElementById('cancel-button'), (event) => clearResult());
|
23901
|
-
|
23902
|
-
const dropdownContent = document.getElementById('submit-dropdown-content');
|
23903
|
-
const dropdownButton = document.getElementById('submit-dropdown-button');
|
23904
|
-
addClickListener(dropdownButton, () => dropdownContent.classList.toggle('submit-dropdown-content-show'));
|
23905
|
-
|
23906
|
-
const isMac = navigator.userAgent.includes('Mac');
|
23907
|
-
const runCurrentLabel = `run selection (${isMac ? '⌘' : 'Ctrl'}-Enter)`;
|
23908
|
-
const runAllLabel = `run all (${isMac ? '⌘' : 'Ctrl'}-Shift-Enter)`;
|
23909
|
-
|
23910
|
-
const submitButtonCurrent = document.getElementById('submit-button-current');
|
23911
|
-
submitButtonCurrent.value = runCurrentLabel;
|
23912
|
-
addClickListener(submitButtonCurrent, (event) => submitCurrent(event.target, event));
|
23913
|
-
|
23914
|
-
const submitButtonAll = document.getElementById('submit-button-all');
|
23915
|
-
submitButtonAll.value = runAllLabel;
|
23916
|
-
addClickListener(submitButtonAll, (event) => submitAll(event.target, event));
|
23917
|
-
|
23918
|
-
const dropdownButtonCurrent = document.getElementById('submit-dropdown-button-current');
|
23919
|
-
dropdownButtonCurrent.value = runCurrentLabel;
|
23920
|
-
addClickListener(dropdownButtonCurrent, (event) => submitCurrent(event.target, event));
|
23921
|
-
|
23922
|
-
const dropdownAllButton = document.getElementById('submit-dropdown-button-all');
|
23923
|
-
dropdownAllButton.value = runAllLabel;
|
23924
|
-
addClickListener(dropdownAllButton, (event) => submitAll(event.target, event));
|
23925
|
-
|
23926
|
-
const dropdownToggleButton = document.getElementById('submit-dropdown-button-toggle');
|
23927
|
-
addClickListener(dropdownToggleButton, () => {
|
23928
|
-
submitButtonCurrent.classList.toggle('submit-button-show');
|
23929
|
-
submitButtonAll.classList.toggle('submit-button-show');
|
23930
|
-
focus(getSelection());
|
23931
|
-
});
|
23932
|
-
|
23933
|
-
const copyListenerFactory = (delimiter) => {
|
23934
|
-
return () => {
|
23935
|
-
if (!window.sqlFetch?.result) {
|
23936
|
-
return
|
23937
|
-
}
|
23938
|
-
const type = 'text/plain';
|
23939
|
-
let text = window.sqlFetch.result.columns.map((header) => {
|
23940
|
-
if (typeof header === 'string' && (header.includes('"') || header.includes(delimiter))) {
|
23941
|
-
return `"${header.replaceAll('"', '""')}"`
|
23942
|
-
} else {
|
23943
|
-
return header
|
23944
|
-
}
|
23945
|
-
}).join(delimiter) + '\n';
|
23946
|
-
text += window.sqlFetch.result.rows.map((row) => {
|
23947
|
-
return row.map((cell) => {
|
23948
|
-
if (typeof cell === 'string' && (cell.includes('"') || cell.includes(delimiter))) {
|
23949
|
-
return `"${cell.replaceAll('"', '""')}"`
|
23950
|
-
} else {
|
23951
|
-
return cell
|
23952
|
-
}
|
23953
|
-
}).join(delimiter)
|
23954
|
-
}).join('\n');
|
23955
|
-
const blob = new Blob([text], { type });
|
23956
|
-
navigator.clipboard.write([new window.ClipboardItem({ [type]: blob })]);
|
23957
|
-
}
|
23958
|
-
};
|
23959
|
-
addClickListener(document.getElementById('submit-dropdown-button-copy-csv'), copyListenerFactory(','));
|
23960
|
-
addClickListener(document.getElementById('submit-dropdown-button-copy-tsv'), copyListenerFactory('\t'));
|
23961
|
-
addClickListener(document.getElementById('submit-dropdown-button-download-csv'), () => {
|
23962
|
-
if (!window.sqlFetch?.result) return
|
23963
|
-
|
23964
|
-
const url = new URL(window.location);
|
23965
|
-
url.searchParams.set('sql', base64Encode(getSqlFromUrl(url)));
|
23966
|
-
url.searchParams.delete('file');
|
23967
|
-
setActionInUrl(url, 'download_csv');
|
23968
|
-
|
23969
|
-
const link = document.createElement('a');
|
23970
|
-
link.setAttribute('download', 'result.csv');
|
23971
|
-
link.setAttribute('href', url.href);
|
23972
|
-
link.click();
|
23973
|
-
|
23974
|
-
focus(getSelection());
|
23975
|
-
});
|
23976
|
-
|
23977
|
-
document.addEventListener('click', function (event) {
|
23978
|
-
if (event.target !== dropdownButton) {
|
23979
|
-
dropdownContent.classList.remove('submit-dropdown-content-show');
|
23980
|
-
}
|
23981
|
-
});
|
23982
|
-
dropdownContent.addEventListener('focusout', function (event) {
|
23983
|
-
if (!dropdownContent.contains(event.relatedTarget)) {
|
23984
|
-
dropdownContent.classList.remove('submit-dropdown-content-show');
|
23985
|
-
}
|
23986
|
-
});
|
23987
|
-
|
23914
|
+
function createEditor (parent, metadata, onSubmit, onShiftSubmit) {
|
23988
23915
|
const fixedHeightEditor = EditorView.theme({
|
23989
23916
|
'.cm-scroller': {
|
23990
23917
|
height: '200px',
|
@@ -23992,22 +23919,23 @@
|
|
23992
23919
|
resize: 'vertical'
|
23993
23920
|
}
|
23994
23921
|
});
|
23995
|
-
const schemas = Object.entries(
|
23922
|
+
const schemas = Object.entries(metadata.schemas);
|
23996
23923
|
const editorSchema = {};
|
23997
23924
|
const tables = [];
|
23998
|
-
const hasTableAliases = Object.keys(window.metadata.table_aliases).length > 0;
|
23999
23925
|
schemas.forEach(([schemaName, schema]) => {
|
24000
23926
|
Object.entries(schema.tables).forEach(([tableName, table]) => {
|
24001
23927
|
const qualifiedTableName = schemas.length === 1 ? tableName : `${schemaName}.${tableName}`;
|
24002
23928
|
const quotedQualifiedTableName = schemas.length === 1 ? `\`${tableName}\`` : `\`${schemaName}\`.\`${tableName}\``;
|
24003
23929
|
const columns = Object.keys(table.columns);
|
24004
23930
|
editorSchema[qualifiedTableName] = columns;
|
24005
|
-
const alias =
|
23931
|
+
const alias = metadata.tables[qualifiedTableName]?.alias;
|
23932
|
+
const boost = metadata.tables[qualifiedTableName]?.boost;
|
24006
23933
|
if (alias) {
|
24007
23934
|
editorSchema[alias] = columns;
|
24008
23935
|
tables.push({
|
24009
23936
|
label: qualifiedTableName,
|
24010
23937
|
detail: alias,
|
23938
|
+
boost,
|
24011
23939
|
alias_type: 'with',
|
24012
23940
|
quoted: `${quotedQualifiedTableName} \`${alias}\``,
|
24013
23941
|
unquoted: `${qualifiedTableName} ${alias}`
|
@@ -24015,6 +23943,7 @@
|
|
24015
23943
|
tables.push({
|
24016
23944
|
label: qualifiedTableName,
|
24017
23945
|
detail: alias,
|
23946
|
+
boost,
|
24018
23947
|
alias_type: 'only',
|
24019
23948
|
quoted: '`' + alias + '`',
|
24020
23949
|
unquoted: alias
|
@@ -24065,7 +23994,7 @@
|
|
24065
23994
|
const originalSchemaCompletionSource = schemaCompletionSource(sqlConfig);
|
24066
23995
|
const originalKeywordCompletionSource = keywordCompletionSource(MySQL, true);
|
24067
23996
|
const keywordCompletions = [];
|
24068
|
-
|
23997
|
+
metadata.joins.forEach((join) => {
|
24069
23998
|
['JOIN', 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'CROSS JOIN'].forEach((type) => {
|
24070
23999
|
keywordCompletions.push({ label: `${type} ${join.label}`, apply: `${type} ${join.apply}`, type: 'keyword' });
|
24071
24000
|
});
|
@@ -24090,7 +24019,7 @@
|
|
24090
24019
|
MySQL.language.data.of({
|
24091
24020
|
autocomplete: (context) => {
|
24092
24021
|
const result = originalSchemaCompletionSource(context);
|
24093
|
-
if (!
|
24022
|
+
if (!result?.options) return result
|
24094
24023
|
|
24095
24024
|
const tree = syntaxTree(context.state);
|
24096
24025
|
let node = tree.resolveInner(context.pos, -1);
|
@@ -24164,10 +24093,19 @@
|
|
24164
24093
|
if (foundSchema) {
|
24165
24094
|
const unquotedLabel = unquoteSqlId(option.label);
|
24166
24095
|
const quoted = unquotedLabel !== option.label;
|
24167
|
-
const
|
24096
|
+
const tableConfig = metadata.tables[`${foundSchema}.${unquotedLabel}`];
|
24097
|
+
const alias = tableConfig?.alias;
|
24098
|
+
const boost = tableConfig?.boost || -1;
|
24099
|
+
const optionOverride = {
|
24100
|
+
label: option.label
|
24101
|
+
};
|
24168
24102
|
if (alias) {
|
24169
|
-
|
24103
|
+
optionOverride.label = quoted ? `\`${unquotedLabel}\` \`${alias}\`` : `${option.label} ${alias}`;
|
24170
24104
|
}
|
24105
|
+
if (boost) {
|
24106
|
+
optionOverride.boost = boost;
|
24107
|
+
}
|
24108
|
+
if (alias || boost) return optionOverride
|
24171
24109
|
}
|
24172
24110
|
return option
|
24173
24111
|
});
|
@@ -24179,7 +24117,7 @@
|
|
24179
24117
|
})
|
24180
24118
|
]
|
24181
24119
|
);
|
24182
|
-
|
24120
|
+
return new EditorView({
|
24183
24121
|
state: EditorState.create({
|
24184
24122
|
extensions: [
|
24185
24123
|
lineNumbers(),
|
@@ -24206,7 +24144,149 @@
|
|
24206
24144
|
]
|
24207
24145
|
}),
|
24208
24146
|
parent
|
24147
|
+
})
|
24148
|
+
}
|
24149
|
+
|
24150
|
+
function unquoteSqlId (identifier) {
|
24151
|
+
const match = identifier.match(/^`(.*)`$/);
|
24152
|
+
return match ? match[1] : identifier
|
24153
|
+
}
|
24154
|
+
|
24155
|
+
function createTable (columns, rows, id, cellRenderer) {
|
24156
|
+
const tableElement = document.createElement('table');
|
24157
|
+
if (id) tableElement.id = id;
|
24158
|
+
const theadElement = document.createElement('thead');
|
24159
|
+
const headerTrElement = document.createElement('tr');
|
24160
|
+
const tbodyElement = document.createElement('tbody');
|
24161
|
+
theadElement.appendChild(headerTrElement);
|
24162
|
+
tableElement.appendChild(theadElement);
|
24163
|
+
tableElement.appendChild(tbodyElement);
|
24164
|
+
|
24165
|
+
columns.forEach(function (columnName) {
|
24166
|
+
const headerElement = document.createElement('th');
|
24167
|
+
headerElement.innerText = columnName;
|
24168
|
+
headerTrElement.appendChild(headerElement);
|
24209
24169
|
});
|
24170
|
+
if (columns.length > 0) {
|
24171
|
+
headerTrElement.appendChild(document.createElement('th'));
|
24172
|
+
}
|
24173
|
+
let highlight = false;
|
24174
|
+
rows.forEach(function (row) {
|
24175
|
+
const rowElement = document.createElement('tr');
|
24176
|
+
if (highlight) {
|
24177
|
+
rowElement.classList.add('highlighted-row');
|
24178
|
+
}
|
24179
|
+
highlight = !highlight;
|
24180
|
+
tbodyElement.appendChild(rowElement);
|
24181
|
+
row.forEach(function (value, i) {
|
24182
|
+
let cellElement;
|
24183
|
+
if (cellRenderer) {
|
24184
|
+
cellElement = cellRenderer(columns[i], value);
|
24185
|
+
} else {
|
24186
|
+
cellElement = document.createElement('td');
|
24187
|
+
cellElement.innerText = value;
|
24188
|
+
}
|
24189
|
+
rowElement.appendChild(cellElement);
|
24190
|
+
});
|
24191
|
+
rowElement.appendChild(document.createElement('td'));
|
24192
|
+
});
|
24193
|
+
return tableElement
|
24194
|
+
}
|
24195
|
+
|
24196
|
+
/* global google */
|
24197
|
+
|
24198
|
+
function getSqlFromUrl (url) {
|
24199
|
+
const params = url.searchParams;
|
24200
|
+
if (params.has('file') && params.has('sql')) {
|
24201
|
+
// TODO: show an error.
|
24202
|
+
throw new Error('You can only specify a file or sql param, not both.')
|
24203
|
+
}
|
24204
|
+
if (params.has('sql')) {
|
24205
|
+
return params.get('sql')
|
24206
|
+
} else if (params.has('file')) {
|
24207
|
+
const file = params.get('file');
|
24208
|
+
const fileDetails = window.metadata.saved[file];
|
24209
|
+
if (!fileDetails) throw new Error(`no such file: ${file}`)
|
24210
|
+
return fileDetails.contents
|
24211
|
+
}
|
24212
|
+
throw new Error('You must specify a file or sql param')
|
24213
|
+
}
|
24214
|
+
|
24215
|
+
function init (parent, onSubmit, onShiftSubmit) {
|
24216
|
+
addClickListener(document.getElementById('query-tab-button'), (event) => selectTab(event, 'query'));
|
24217
|
+
addClickListener(document.getElementById('saved-tab-button'), (event) => selectTab(event, 'saved'));
|
24218
|
+
addClickListener(document.getElementById('structure-tab-button'), (event) => selectTab(event, 'structure'));
|
24219
|
+
addClickListener(document.getElementById('graph-tab-button'), (event) => selectTab(event, 'graph'));
|
24220
|
+
addClickListener(document.getElementById('cancel-button'), (event) => clearResult());
|
24221
|
+
|
24222
|
+
const dropdownContent = document.getElementById('submit-dropdown-content');
|
24223
|
+
const dropdownButton = document.getElementById('submit-dropdown-button');
|
24224
|
+
addClickListener(dropdownButton, () => dropdownContent.classList.toggle('submit-dropdown-content-show'));
|
24225
|
+
|
24226
|
+
const isMac = navigator.userAgent.includes('Mac');
|
24227
|
+
const runCurrentLabel = `run selection (${isMac ? '⌘' : 'Ctrl'}-Enter)`;
|
24228
|
+
const runAllLabel = `run all (${isMac ? '⌘' : 'Ctrl'}-Shift-Enter)`;
|
24229
|
+
|
24230
|
+
const submitButtonCurrent = document.getElementById('submit-button-current');
|
24231
|
+
submitButtonCurrent.value = runCurrentLabel;
|
24232
|
+
addClickListener(submitButtonCurrent, (event) => submitCurrent(event.target, event));
|
24233
|
+
|
24234
|
+
const submitButtonAll = document.getElementById('submit-button-all');
|
24235
|
+
submitButtonAll.value = runAllLabel;
|
24236
|
+
addClickListener(submitButtonAll, (event) => submitAll(event.target, event));
|
24237
|
+
|
24238
|
+
const dropdownButtonCurrent = document.getElementById('submit-dropdown-button-current');
|
24239
|
+
dropdownButtonCurrent.value = runCurrentLabel;
|
24240
|
+
addClickListener(dropdownButtonCurrent, (event) => submitCurrent(event.target, event));
|
24241
|
+
|
24242
|
+
const dropdownAllButton = document.getElementById('submit-dropdown-button-all');
|
24243
|
+
dropdownAllButton.value = runAllLabel;
|
24244
|
+
addClickListener(dropdownAllButton, (event) => submitAll(event.target, event));
|
24245
|
+
|
24246
|
+
const dropdownToggleButton = document.getElementById('submit-dropdown-button-toggle');
|
24247
|
+
addClickListener(dropdownToggleButton, () => {
|
24248
|
+
submitButtonCurrent.classList.toggle('submit-button-show');
|
24249
|
+
submitButtonAll.classList.toggle('submit-button-show');
|
24250
|
+
focus(getSelection());
|
24251
|
+
});
|
24252
|
+
|
24253
|
+
addClickListener(document.getElementById('submit-dropdown-button-copy-csv'), (event) => {
|
24254
|
+
if (window.sqlFetch?.result) {
|
24255
|
+
copyTextToClipboard(toCsv(window.sqlFetch.result.columns, window.sqlFetch.result.rows));
|
24256
|
+
}
|
24257
|
+
});
|
24258
|
+
addClickListener(document.getElementById('submit-dropdown-button-copy-tsv'), (event) => {
|
24259
|
+
if (window.sqlFetch?.result) {
|
24260
|
+
copyTextToClipboard(toTsv(window.sqlFetch.result.columns, window.sqlFetch.result.rows));
|
24261
|
+
}
|
24262
|
+
});
|
24263
|
+
addClickListener(document.getElementById('submit-dropdown-button-download-csv'), () => {
|
24264
|
+
if (!window.sqlFetch?.result) return
|
24265
|
+
|
24266
|
+
const url = new URL(window.location);
|
24267
|
+
url.searchParams.set('sql', base64Encode(getSqlFromUrl(url)));
|
24268
|
+
url.searchParams.delete('file');
|
24269
|
+
setActionInUrl(url, 'download_csv');
|
24270
|
+
|
24271
|
+
const link = document.createElement('a');
|
24272
|
+
link.setAttribute('download', 'result.csv');
|
24273
|
+
link.setAttribute('href', url.href);
|
24274
|
+
link.click();
|
24275
|
+
|
24276
|
+
focus(getSelection());
|
24277
|
+
});
|
24278
|
+
|
24279
|
+
document.addEventListener('click', function (event) {
|
24280
|
+
if (event.target !== dropdownButton) {
|
24281
|
+
dropdownContent.classList.remove('submit-dropdown-content-show');
|
24282
|
+
}
|
24283
|
+
});
|
24284
|
+
dropdownContent.addEventListener('focusout', function (event) {
|
24285
|
+
if (!dropdownContent.contains(event.relatedTarget)) {
|
24286
|
+
dropdownContent.classList.remove('submit-dropdown-content-show');
|
24287
|
+
}
|
24288
|
+
});
|
24289
|
+
window.editorView = createEditor(parent, window.metadata, onSubmit, onShiftSubmit);
|
24210
24290
|
}
|
24211
24291
|
|
24212
24292
|
function addClickListener (element, func) {
|
@@ -24427,7 +24507,7 @@
|
|
24427
24507
|
}
|
24428
24508
|
rows.push(row);
|
24429
24509
|
}
|
24430
|
-
createTable(
|
24510
|
+
columnsElement.appendChild(createTable(columns, rows, null));
|
24431
24511
|
}
|
24432
24512
|
|
24433
24513
|
const indexEntries = Object.entries(table.indexes);
|
@@ -24447,49 +24527,12 @@
|
|
24447
24527
|
rows.push(row);
|
24448
24528
|
}
|
24449
24529
|
}
|
24450
|
-
createTable(
|
24530
|
+
indexesElement.appendChild(createTable(columns, rows, null));
|
24451
24531
|
}
|
24452
24532
|
});
|
24453
24533
|
window.structureLoaded = true;
|
24454
24534
|
}
|
24455
24535
|
|
24456
|
-
function createTable (parent, columns, rows) {
|
24457
|
-
const tableElement = document.createElement('table');
|
24458
|
-
const theadElement = document.createElement('thead');
|
24459
|
-
const headerTrElement = document.createElement('tr');
|
24460
|
-
const tbodyElement = document.createElement('tbody');
|
24461
|
-
theadElement.appendChild(headerTrElement);
|
24462
|
-
tableElement.appendChild(theadElement);
|
24463
|
-
tableElement.appendChild(tbodyElement);
|
24464
|
-
parent.appendChild(tableElement);
|
24465
|
-
|
24466
|
-
columns.forEach(function (columnName) {
|
24467
|
-
const headerElement = document.createElement('th');
|
24468
|
-
headerElement.classList.add('cell');
|
24469
|
-
headerElement.innerText = columnName;
|
24470
|
-
headerTrElement.appendChild(headerElement);
|
24471
|
-
});
|
24472
|
-
if (columns.length > 0) {
|
24473
|
-
headerTrElement.appendChild(document.createElement('th'));
|
24474
|
-
}
|
24475
|
-
let highlight = false;
|
24476
|
-
rows.forEach(function (row) {
|
24477
|
-
const rowElement = document.createElement('tr');
|
24478
|
-
if (highlight) {
|
24479
|
-
rowElement.classList.add('highlighted-row');
|
24480
|
-
}
|
24481
|
-
highlight = !highlight;
|
24482
|
-
tbodyElement.appendChild(rowElement);
|
24483
|
-
row.forEach(function (value) {
|
24484
|
-
const cellElement = document.createElement('td');
|
24485
|
-
cellElement.classList.add('cell');
|
24486
|
-
cellElement.innerText = value;
|
24487
|
-
rowElement.appendChild(cellElement);
|
24488
|
-
});
|
24489
|
-
rowElement.appendChild(document.createElement('td'));
|
24490
|
-
});
|
24491
|
-
}
|
24492
|
-
|
24493
24536
|
function selectGraphTab (internal) {
|
24494
24537
|
document.getElementById('query-box').style.display = 'flex';
|
24495
24538
|
document.getElementById('submit-box').style.display = 'flex';
|
@@ -24881,39 +24924,32 @@
|
|
24881
24924
|
clearResultBox();
|
24882
24925
|
displaySqlFetchResultStatus('result-status', fetch);
|
24883
24926
|
|
24884
|
-
const
|
24885
|
-
|
24886
|
-
|
24887
|
-
|
24888
|
-
|
24889
|
-
theadElement.appendChild(headerElement);
|
24890
|
-
tableElement.appendChild(theadElement);
|
24891
|
-
tableElement.appendChild(tbodyElement);
|
24892
|
-
document.getElementById('result-box').appendChild(tableElement);
|
24927
|
+
const createLink = function (link, value) {
|
24928
|
+
const linkElement = document.createElement('a');
|
24929
|
+
linkElement.href = link.template.replaceAll('{*}', encodeURIComponent(value));
|
24930
|
+
linkElement.innerText = link.short_name;
|
24931
|
+
linkElement.target = '_blank';
|
24893
24932
|
|
24894
|
-
|
24895
|
-
|
24896
|
-
|
24897
|
-
|
24898
|
-
|
24899
|
-
|
24900
|
-
|
24901
|
-
|
24902
|
-
|
24903
|
-
|
24904
|
-
|
24905
|
-
|
24906
|
-
|
24933
|
+
const abbrElement = document.createElement('abbr');
|
24934
|
+
abbrElement.title = link.long_name;
|
24935
|
+
abbrElement.appendChild(linkElement);
|
24936
|
+
|
24937
|
+
return abbrElement
|
24938
|
+
};
|
24939
|
+
const cellRenderer = function (column, value) {
|
24940
|
+
const cellElement = document.createElement('td');
|
24941
|
+
if (window.metadata.columns[column]?.links?.length > 0) {
|
24942
|
+
cellElement.appendChild(document.createTextNode(value));
|
24943
|
+
window.metadata.columns[column].links.forEach((link) => {
|
24944
|
+
cellElement.appendChild(createLink(link, value));
|
24945
|
+
});
|
24946
|
+
} else {
|
24947
|
+
cellElement.innerText = value;
|
24907
24948
|
}
|
24908
|
-
|
24909
|
-
|
24910
|
-
|
24911
|
-
|
24912
|
-
template.innerHTML = `<td class="cell">${value}</td>`;
|
24913
|
-
rowElement.appendChild(template.content.firstChild);
|
24914
|
-
});
|
24915
|
-
rowElement.appendChild(document.createElement('td'));
|
24916
|
-
});
|
24949
|
+
return cellElement
|
24950
|
+
};
|
24951
|
+
document.getElementById('result-box')
|
24952
|
+
.appendChild(createTable(fetch.result.columns, fetch.result.rows, 'result-table', cellRenderer));
|
24917
24953
|
}
|
24918
24954
|
|
24919
24955
|
function disableDownloadButtons () {
|
@@ -25105,10 +25141,12 @@
|
|
25105
25141
|
if (contentType && contentType.indexOf('application/json') !== -1) {
|
25106
25142
|
return response.json().then((result) => {
|
25107
25143
|
if (result.error) {
|
25108
|
-
let error =
|
25144
|
+
let error = '<div style="font-family: monospace; font-size: 16px;">\n';
|
25145
|
+
error += `<div>${result.error}</div>\n`;
|
25109
25146
|
if (result.stacktrace) {
|
25110
|
-
error += '
|
25147
|
+
error += '<pre>\n' + result.stacktrace + '\n</pre>\n';
|
25111
25148
|
}
|
25149
|
+
error += '</div>\n';
|
25112
25150
|
document.getElementById('loading-box').innerHTML = error;
|
25113
25151
|
} else if (!result.server) {
|
25114
25152
|
document.getElementById('loading-box').innerHTML = `
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sqlui
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.48
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Dower
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-11-
|
11
|
+
date: 2022-11-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mysql2
|
@@ -140,7 +140,9 @@ files:
|
|
140
140
|
- app/sqlui.rb
|
141
141
|
- app/sqlui_config.rb
|
142
142
|
- app/views/databases.erb
|
143
|
+
- app/views/error.erb
|
143
144
|
- bin/sqlui
|
145
|
+
- client/resources/favicon.svg
|
144
146
|
- client/resources/sqlui.css
|
145
147
|
- client/resources/sqlui.html
|
146
148
|
- client/resources/sqlui.js
|