sqlui 0.1.47 → 0.1.49
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/args.rb +6 -6
- data/app/database_config.rb +34 -4
- data/app/server.rb +17 -9
- data/app/sql_parser.rb +74 -10
- data/app/views/error.erb +21 -0
- data/client/resources/favicon.svg +1 -0
- data/client/resources/sqlui.css +24 -3
- data/client/resources/sqlui.html +1 -0
- data/client/resources/sqlui.js +247 -203
- 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: ae60c3a8d2bb051b87f0e10ed67b3c5e5c6360ecd298b5176a17a7a5f66796d8
|
4
|
+
data.tar.gz: 4ee88fae0167dc7dd8958384d7b2492df3944368238453d729244d91232ebf35
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b809f370ad688d9b54d50ff1ccf4dd703dea5af4344f61577220da2b3f4d7536265d8b8e03613158b81fdec4d86ca10dde35f89623cb114fde2b345a5df0108b
|
7
|
+
data.tar.gz: 70324412047c434e03c39e3676cd616da4f6a29c0c7767805fe4073d15e59dd1f28d994253921e18457c469154661081a4570cfec63100cfb9dd2d3ae1aec242
|
data/.version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.49
|
data/app/args.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
class Args
|
5
5
|
def self.fetch_non_empty_string(hash, key)
|
6
6
|
value = fetch_non_nil(hash, key, String)
|
7
|
-
raise ArgumentError, "
|
7
|
+
raise ArgumentError, "parameter #{key} empty" if value.strip.empty?
|
8
8
|
|
9
9
|
value
|
10
10
|
end
|
@@ -15,7 +15,7 @@ class Args
|
|
15
15
|
|
16
16
|
def self.fetch_non_empty_hash(hash, key)
|
17
17
|
value = fetch_non_nil(hash, key, Hash)
|
18
|
-
raise ArgumentError, "
|
18
|
+
raise ArgumentError, "parameter #{key} empty" if value.empty?
|
19
19
|
|
20
20
|
value
|
21
21
|
end
|
@@ -29,9 +29,9 @@ class Args
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def self.fetch_non_nil(hash, key, *classes)
|
32
|
-
raise ArgumentError, "
|
32
|
+
raise ArgumentError, "parameter #{key} missing" unless hash.key?(key)
|
33
33
|
|
34
|
-
raise ArgumentError, "
|
34
|
+
raise ArgumentError, "parameter #{key} null" if hash[key].nil?
|
35
35
|
|
36
36
|
fetch_optional(hash, key, *classes)
|
37
37
|
end
|
@@ -40,12 +40,12 @@ class Args
|
|
40
40
|
value = hash[key]
|
41
41
|
if value && classes.size.positive? && !classes.find { |clazz| value.is_a?(clazz) }
|
42
42
|
if classes.size != 1
|
43
|
-
raise ArgumentError, "
|
43
|
+
raise ArgumentError, "parameter #{key} not #{classes.map(&:to_s).map(&:downcase).join(' or ')}"
|
44
44
|
end
|
45
45
|
|
46
46
|
class_name = classes[0].to_s.downcase
|
47
47
|
class_name = %w[a e i o u].include?(class_name[0]) ? "an #{class_name}" : "a #{class_name}"
|
48
|
-
raise ArgumentError, "
|
48
|
+
raise ArgumentError, "parameter #{key} not #{class_name}"
|
49
49
|
end
|
50
50
|
|
51
51
|
value
|
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, :tables, :client_params
|
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
|
@@ -18,8 +18,10 @@ class DatabaseConfig
|
|
18
18
|
raise ArgumentError, 'url_path should not end with a /' if @url_path.length > 1 && @url_path.end_with?('/')
|
19
19
|
|
20
20
|
@saved_path = Args.fetch_non_empty_string(hash, :saved_path).strip
|
21
|
-
|
22
|
-
|
21
|
+
|
22
|
+
# Make joins an array. It is only a map to allow for YAML extension.
|
23
|
+
@joins = (Args.fetch_optional_hash(hash, :joins) || {}).values
|
24
|
+
@joins.each do |join|
|
23
25
|
next if join.is_a?(Hash) &&
|
24
26
|
join.keys.size == 2 &&
|
25
27
|
join[:label].is_a?(String) && !join[:label].strip.empty? &&
|
@@ -27,8 +29,9 @@ class DatabaseConfig
|
|
27
29
|
|
28
30
|
raise ArgumentError, "invalid join #{join.to_json}"
|
29
31
|
end
|
32
|
+
|
30
33
|
@tables = Args.fetch_optional_hash(hash, :tables) || {}
|
31
|
-
@tables
|
34
|
+
@tables.each do |table, table_config|
|
32
35
|
unless table_config.is_a?(Hash)
|
33
36
|
raise ArgumentError, "invalid table config for #{table} (#{table_config}), expected hash"
|
34
37
|
end
|
@@ -43,6 +46,33 @@ class DatabaseConfig
|
|
43
46
|
raise ArgumentError, "invalid table boost for #{table} (#{table_boost}), expected int"
|
44
47
|
end
|
45
48
|
end
|
49
|
+
|
50
|
+
@columns = Args.fetch_optional_hash(hash, :columns) || {}
|
51
|
+
@columns.each do |column, column_config|
|
52
|
+
unless column_config.is_a?(Hash)
|
53
|
+
raise ArgumentError, "invalid column config for #{column} (#{column_config}), expected hash"
|
54
|
+
end
|
55
|
+
|
56
|
+
links = Args.fetch_optional_hash(column_config, :links) || {}
|
57
|
+
links.each_value do |link_config|
|
58
|
+
unless link_config.is_a?(Hash)
|
59
|
+
raise ArgumentError, "invalid link config for #{column} (#{link_config}), expected hash"
|
60
|
+
end
|
61
|
+
|
62
|
+
unless link_config[:short_name].is_a?(String)
|
63
|
+
raise ArgumentError,
|
64
|
+
"invalid link short_name for #{column} link (#{link_config[:short_name]}), expected string"
|
65
|
+
end
|
66
|
+
unless link_config[:long_name].is_a?(String)
|
67
|
+
raise ArgumentError, "invalid link long_name for #{column} link (#{link_config[:long_name]}), expected string"
|
68
|
+
end
|
69
|
+
unless link_config[:template].is_a?(String)
|
70
|
+
raise ArgumentError, "invalid link template for #{column} link (#{link_config[:template]}), expected string"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
# Make links an array. It is only a map to allow for YAML extension
|
74
|
+
column_config[:links] = links.values
|
75
|
+
end
|
46
76
|
aliases = @tables.map { |_table, table_config| table_config[:alias] }.compact
|
47
77
|
if aliases.to_set.size < aliases.size
|
48
78
|
duplicate_aliases = aliases.reject { |a| aliases.count(a) == 1 }.to_set
|
data/app/server.rb
CHANGED
@@ -39,6 +39,10 @@ class Server < Sinatra::Base
|
|
39
39
|
redirect config.list_url_path, 301
|
40
40
|
end
|
41
41
|
|
42
|
+
get '/favicon.svg' do
|
43
|
+
send_file File.join(resources_dir, 'favicon.svg')
|
44
|
+
end
|
45
|
+
|
42
46
|
get "#{config.list_url_path}/?" do
|
43
47
|
erb :databases, locals: { config: config }
|
44
48
|
end
|
@@ -69,6 +73,7 @@ class Server < Sinatra::Base
|
|
69
73
|
list_url_path: config.list_url_path,
|
70
74
|
schemas: DatabaseMetadata.lookup(client, database),
|
71
75
|
tables: database.tables,
|
76
|
+
columns: database.columns,
|
72
77
|
joins: database.joins,
|
73
78
|
saved: Dir.glob("#{database.saved_path}/*.sql").to_h do |path|
|
74
79
|
contents = File.read(path)
|
@@ -163,15 +168,18 @@ class Server < Sinatra::Base
|
|
163
168
|
end
|
164
169
|
end
|
165
170
|
|
166
|
-
error do
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
175
183
|
end
|
176
184
|
|
177
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/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,27 @@ 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 5px 0 0;
|
262
|
+
text-decoration: none;
|
263
|
+
position: relative;
|
264
|
+
}
|
265
|
+
|
266
|
+
#result-table tbody tr td abbr:last-child {
|
267
|
+
margin: 0;
|
268
|
+
}
|
269
|
+
|
249
270
|
.fetch-sql-box {
|
250
271
|
justify-content: center;
|
251
272
|
align-items: center;
|
@@ -271,7 +292,7 @@ table td:last-child, table th:last-child {
|
|
271
292
|
}
|
272
293
|
|
273
294
|
td, th {
|
274
|
-
padding: 5px
|
295
|
+
padding: 5px 10px;
|
275
296
|
font-weight: normal;
|
276
297
|
white-space: nowrap;
|
277
298
|
max-width: 500px;
|
@@ -300,7 +321,7 @@ thead {
|
|
300
321
|
position: -webkit-sticky;
|
301
322
|
position: sticky;
|
302
323
|
top: 0;
|
303
|
-
z-index:
|
324
|
+
z-index: 1;
|
304
325
|
table-layout: fixed;
|
305
326
|
}
|
306
327
|
|
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,7 +23919,7 @@
|
|
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
23925
|
schemas.forEach(([schemaName, schema]) => {
|
@@ -24001,8 +23928,8 @@
|
|
24001
23928
|
const quotedQualifiedTableName = schemas.length === 1 ? `\`${tableName}\`` : `\`${schemaName}\`.\`${tableName}\``;
|
24002
23929
|
const columns = Object.keys(table.columns);
|
24003
23930
|
editorSchema[qualifiedTableName] = columns;
|
24004
|
-
const alias =
|
24005
|
-
const boost =
|
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({
|
@@ -24067,7 +23994,7 @@
|
|
24067
23994
|
const originalSchemaCompletionSource = schemaCompletionSource(sqlConfig);
|
24068
23995
|
const originalKeywordCompletionSource = keywordCompletionSource(MySQL, true);
|
24069
23996
|
const keywordCompletions = [];
|
24070
|
-
|
23997
|
+
metadata.joins.forEach((join) => {
|
24071
23998
|
['JOIN', 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'CROSS JOIN'].forEach((type) => {
|
24072
23999
|
keywordCompletions.push({ label: `${type} ${join.label}`, apply: `${type} ${join.apply}`, type: 'keyword' });
|
24073
24000
|
});
|
@@ -24166,7 +24093,7 @@
|
|
24166
24093
|
if (foundSchema) {
|
24167
24094
|
const unquotedLabel = unquoteSqlId(option.label);
|
24168
24095
|
const quoted = unquotedLabel !== option.label;
|
24169
|
-
const tableConfig =
|
24096
|
+
const tableConfig = metadata.tables[`${foundSchema}.${unquotedLabel}`];
|
24170
24097
|
const alias = tableConfig?.alias;
|
24171
24098
|
const boost = tableConfig?.boost || -1;
|
24172
24099
|
const optionOverride = {
|
@@ -24190,7 +24117,7 @@
|
|
24190
24117
|
})
|
24191
24118
|
]
|
24192
24119
|
);
|
24193
|
-
|
24120
|
+
return new EditorView({
|
24194
24121
|
state: EditorState.create({
|
24195
24122
|
extensions: [
|
24196
24123
|
lineNumbers(),
|
@@ -24217,7 +24144,152 @@
|
|
24217
24144
|
]
|
24218
24145
|
}),
|
24219
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, headerRenderer, 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
|
+
if (headerRenderer) {
|
24167
|
+
headerRenderer(headerTrElement, columnName);
|
24168
|
+
} else {
|
24169
|
+
const headerElement = document.createElement('th');
|
24170
|
+
headerElement.innerText = columnName;
|
24171
|
+
headerTrElement.appendChild(headerElement);
|
24172
|
+
}
|
24173
|
+
});
|
24174
|
+
if (columns.length > 0) {
|
24175
|
+
headerTrElement.appendChild(document.createElement('th'));
|
24176
|
+
}
|
24177
|
+
let highlight = false;
|
24178
|
+
rows.forEach(function (row) {
|
24179
|
+
const rowElement = document.createElement('tr');
|
24180
|
+
if (highlight) {
|
24181
|
+
rowElement.classList.add('highlighted-row');
|
24182
|
+
}
|
24183
|
+
highlight = !highlight;
|
24184
|
+
tbodyElement.appendChild(rowElement);
|
24185
|
+
row.forEach(function (value, i) {
|
24186
|
+
if (cellRenderer) {
|
24187
|
+
cellRenderer(rowElement, columns[i], value);
|
24188
|
+
} else {
|
24189
|
+
const cellElement = document.createElement('td');
|
24190
|
+
cellElement.innerText = value;
|
24191
|
+
rowElement.appendChild(cellElement);
|
24192
|
+
}
|
24193
|
+
});
|
24194
|
+
rowElement.appendChild(document.createElement('td'));
|
24195
|
+
});
|
24196
|
+
return tableElement
|
24197
|
+
}
|
24198
|
+
|
24199
|
+
/* global google */
|
24200
|
+
|
24201
|
+
function getSqlFromUrl (url) {
|
24202
|
+
const params = url.searchParams;
|
24203
|
+
if (params.has('file') && params.has('sql')) {
|
24204
|
+
// TODO: show an error.
|
24205
|
+
throw new Error('You can only specify a file or sql param, not both.')
|
24206
|
+
}
|
24207
|
+
if (params.has('sql')) {
|
24208
|
+
return params.get('sql')
|
24209
|
+
} else if (params.has('file')) {
|
24210
|
+
const file = params.get('file');
|
24211
|
+
const fileDetails = window.metadata.saved[file];
|
24212
|
+
if (!fileDetails) throw new Error(`no such file: ${file}`)
|
24213
|
+
return fileDetails.contents
|
24214
|
+
}
|
24215
|
+
throw new Error('You must specify a file or sql param')
|
24216
|
+
}
|
24217
|
+
|
24218
|
+
function init (parent, onSubmit, onShiftSubmit) {
|
24219
|
+
addClickListener(document.getElementById('query-tab-button'), (event) => selectTab(event, 'query'));
|
24220
|
+
addClickListener(document.getElementById('saved-tab-button'), (event) => selectTab(event, 'saved'));
|
24221
|
+
addClickListener(document.getElementById('structure-tab-button'), (event) => selectTab(event, 'structure'));
|
24222
|
+
addClickListener(document.getElementById('graph-tab-button'), (event) => selectTab(event, 'graph'));
|
24223
|
+
addClickListener(document.getElementById('cancel-button'), (event) => clearResult());
|
24224
|
+
|
24225
|
+
const dropdownContent = document.getElementById('submit-dropdown-content');
|
24226
|
+
const dropdownButton = document.getElementById('submit-dropdown-button');
|
24227
|
+
addClickListener(dropdownButton, () => dropdownContent.classList.toggle('submit-dropdown-content-show'));
|
24228
|
+
|
24229
|
+
const isMac = navigator.userAgent.includes('Mac');
|
24230
|
+
const runCurrentLabel = `run selection (${isMac ? '⌘' : 'Ctrl'}-Enter)`;
|
24231
|
+
const runAllLabel = `run all (${isMac ? '⌘' : 'Ctrl'}-Shift-Enter)`;
|
24232
|
+
|
24233
|
+
const submitButtonCurrent = document.getElementById('submit-button-current');
|
24234
|
+
submitButtonCurrent.value = runCurrentLabel;
|
24235
|
+
addClickListener(submitButtonCurrent, (event) => submitCurrent(event.target, event));
|
24236
|
+
|
24237
|
+
const submitButtonAll = document.getElementById('submit-button-all');
|
24238
|
+
submitButtonAll.value = runAllLabel;
|
24239
|
+
addClickListener(submitButtonAll, (event) => submitAll(event.target, event));
|
24240
|
+
|
24241
|
+
const dropdownButtonCurrent = document.getElementById('submit-dropdown-button-current');
|
24242
|
+
dropdownButtonCurrent.value = runCurrentLabel;
|
24243
|
+
addClickListener(dropdownButtonCurrent, (event) => submitCurrent(event.target, event));
|
24244
|
+
|
24245
|
+
const dropdownAllButton = document.getElementById('submit-dropdown-button-all');
|
24246
|
+
dropdownAllButton.value = runAllLabel;
|
24247
|
+
addClickListener(dropdownAllButton, (event) => submitAll(event.target, event));
|
24248
|
+
|
24249
|
+
const dropdownToggleButton = document.getElementById('submit-dropdown-button-toggle');
|
24250
|
+
addClickListener(dropdownToggleButton, () => {
|
24251
|
+
submitButtonCurrent.classList.toggle('submit-button-show');
|
24252
|
+
submitButtonAll.classList.toggle('submit-button-show');
|
24253
|
+
focus(getSelection());
|
24254
|
+
});
|
24255
|
+
|
24256
|
+
addClickListener(document.getElementById('submit-dropdown-button-copy-csv'), (event) => {
|
24257
|
+
if (window.sqlFetch?.result) {
|
24258
|
+
copyTextToClipboard(toCsv(window.sqlFetch.result.columns, window.sqlFetch.result.rows));
|
24259
|
+
}
|
24260
|
+
});
|
24261
|
+
addClickListener(document.getElementById('submit-dropdown-button-copy-tsv'), (event) => {
|
24262
|
+
if (window.sqlFetch?.result) {
|
24263
|
+
copyTextToClipboard(toTsv(window.sqlFetch.result.columns, window.sqlFetch.result.rows));
|
24264
|
+
}
|
24265
|
+
});
|
24266
|
+
addClickListener(document.getElementById('submit-dropdown-button-download-csv'), () => {
|
24267
|
+
if (!window.sqlFetch?.result) return
|
24268
|
+
|
24269
|
+
const url = new URL(window.location);
|
24270
|
+
url.searchParams.set('sql', base64Encode(getSqlFromUrl(url)));
|
24271
|
+
url.searchParams.delete('file');
|
24272
|
+
setActionInUrl(url, 'download_csv');
|
24273
|
+
|
24274
|
+
const link = document.createElement('a');
|
24275
|
+
link.setAttribute('download', 'result.csv');
|
24276
|
+
link.setAttribute('href', url.href);
|
24277
|
+
link.click();
|
24278
|
+
|
24279
|
+
focus(getSelection());
|
24280
|
+
});
|
24281
|
+
|
24282
|
+
document.addEventListener('click', function (event) {
|
24283
|
+
if (event.target !== dropdownButton) {
|
24284
|
+
dropdownContent.classList.remove('submit-dropdown-content-show');
|
24285
|
+
}
|
24286
|
+
});
|
24287
|
+
dropdownContent.addEventListener('focusout', function (event) {
|
24288
|
+
if (!dropdownContent.contains(event.relatedTarget)) {
|
24289
|
+
dropdownContent.classList.remove('submit-dropdown-content-show');
|
24290
|
+
}
|
24220
24291
|
});
|
24292
|
+
window.editorView = createEditor(parent, window.metadata, onSubmit, onShiftSubmit);
|
24221
24293
|
}
|
24222
24294
|
|
24223
24295
|
function addClickListener (element, func) {
|
@@ -24438,7 +24510,7 @@
|
|
24438
24510
|
}
|
24439
24511
|
rows.push(row);
|
24440
24512
|
}
|
24441
|
-
createTable(
|
24513
|
+
columnsElement.appendChild(createTable(columns, rows, null));
|
24442
24514
|
}
|
24443
24515
|
|
24444
24516
|
const indexEntries = Object.entries(table.indexes);
|
@@ -24458,49 +24530,12 @@
|
|
24458
24530
|
rows.push(row);
|
24459
24531
|
}
|
24460
24532
|
}
|
24461
|
-
createTable(
|
24533
|
+
indexesElement.appendChild(createTable(columns, rows, null));
|
24462
24534
|
}
|
24463
24535
|
});
|
24464
24536
|
window.structureLoaded = true;
|
24465
24537
|
}
|
24466
24538
|
|
24467
|
-
function createTable (parent, columns, rows) {
|
24468
|
-
const tableElement = document.createElement('table');
|
24469
|
-
const theadElement = document.createElement('thead');
|
24470
|
-
const headerTrElement = document.createElement('tr');
|
24471
|
-
const tbodyElement = document.createElement('tbody');
|
24472
|
-
theadElement.appendChild(headerTrElement);
|
24473
|
-
tableElement.appendChild(theadElement);
|
24474
|
-
tableElement.appendChild(tbodyElement);
|
24475
|
-
parent.appendChild(tableElement);
|
24476
|
-
|
24477
|
-
columns.forEach(function (columnName) {
|
24478
|
-
const headerElement = document.createElement('th');
|
24479
|
-
headerElement.classList.add('cell');
|
24480
|
-
headerElement.innerText = columnName;
|
24481
|
-
headerTrElement.appendChild(headerElement);
|
24482
|
-
});
|
24483
|
-
if (columns.length > 0) {
|
24484
|
-
headerTrElement.appendChild(document.createElement('th'));
|
24485
|
-
}
|
24486
|
-
let highlight = false;
|
24487
|
-
rows.forEach(function (row) {
|
24488
|
-
const rowElement = document.createElement('tr');
|
24489
|
-
if (highlight) {
|
24490
|
-
rowElement.classList.add('highlighted-row');
|
24491
|
-
}
|
24492
|
-
highlight = !highlight;
|
24493
|
-
tbodyElement.appendChild(rowElement);
|
24494
|
-
row.forEach(function (value) {
|
24495
|
-
const cellElement = document.createElement('td');
|
24496
|
-
cellElement.classList.add('cell');
|
24497
|
-
cellElement.innerText = value;
|
24498
|
-
rowElement.appendChild(cellElement);
|
24499
|
-
});
|
24500
|
-
rowElement.appendChild(document.createElement('td'));
|
24501
|
-
});
|
24502
|
-
}
|
24503
|
-
|
24504
24539
|
function selectGraphTab (internal) {
|
24505
24540
|
document.getElementById('query-box').style.display = 'flex';
|
24506
24541
|
document.getElementById('submit-box').style.display = 'flex';
|
@@ -24892,39 +24927,46 @@
|
|
24892
24927
|
clearResultBox();
|
24893
24928
|
displaySqlFetchResultStatus('result-status', fetch);
|
24894
24929
|
|
24895
|
-
const
|
24896
|
-
|
24897
|
-
|
24898
|
-
|
24899
|
-
|
24900
|
-
theadElement.appendChild(headerElement);
|
24901
|
-
tableElement.appendChild(theadElement);
|
24902
|
-
tableElement.appendChild(tbodyElement);
|
24903
|
-
document.getElementById('result-box').appendChild(tableElement);
|
24930
|
+
const createLink = function (link, value) {
|
24931
|
+
const linkElement = document.createElement('a');
|
24932
|
+
linkElement.href = link.template.replaceAll('{*}', encodeURIComponent(value));
|
24933
|
+
linkElement.innerText = link.short_name;
|
24934
|
+
linkElement.target = '_blank';
|
24904
24935
|
|
24905
|
-
|
24906
|
-
|
24907
|
-
|
24908
|
-
|
24909
|
-
|
24910
|
-
|
24911
|
-
|
24912
|
-
|
24913
|
-
|
24914
|
-
|
24915
|
-
|
24916
|
-
|
24917
|
-
rowElement.classList.add('highlighted-row');
|
24936
|
+
const abbrElement = document.createElement('abbr');
|
24937
|
+
abbrElement.title = link.long_name;
|
24938
|
+
abbrElement.appendChild(linkElement);
|
24939
|
+
|
24940
|
+
return abbrElement
|
24941
|
+
};
|
24942
|
+
|
24943
|
+
const headerRenderer = function (rowElement, column) {
|
24944
|
+
const headerElement = document.createElement('th');
|
24945
|
+
headerElement.innerText = column;
|
24946
|
+
if (window.metadata.columns[column]) {
|
24947
|
+
headerElement.colSpan = 2;
|
24918
24948
|
}
|
24919
|
-
|
24920
|
-
|
24921
|
-
|
24922
|
-
|
24923
|
-
|
24924
|
-
|
24925
|
-
|
24926
|
-
|
24927
|
-
|
24949
|
+
rowElement.appendChild(headerElement);
|
24950
|
+
};
|
24951
|
+
|
24952
|
+
const cellRenderer = function (rowElement, column, value) {
|
24953
|
+
if (window.metadata.columns[column]?.links?.length > 0) {
|
24954
|
+
const linksColumnElement = document.createElement('td');
|
24955
|
+
window.metadata.columns[column].links.forEach((link) => {
|
24956
|
+
linksColumnElement.appendChild(createLink(link, value));
|
24957
|
+
});
|
24958
|
+
rowElement.appendChild(linksColumnElement);
|
24959
|
+
const textColumnElement = document.createElement('td');
|
24960
|
+
textColumnElement.innerText = value;
|
24961
|
+
rowElement.appendChild(textColumnElement);
|
24962
|
+
} else {
|
24963
|
+
const cellElement = document.createElement('td');
|
24964
|
+
cellElement.innerText = value;
|
24965
|
+
rowElement.appendChild(cellElement);
|
24966
|
+
}
|
24967
|
+
};
|
24968
|
+
document.getElementById('result-box')
|
24969
|
+
.appendChild(createTable(fetch.result.columns, fetch.result.rows, 'result-table', headerRenderer, cellRenderer));
|
24928
24970
|
}
|
24929
24971
|
|
24930
24972
|
function disableDownloadButtons () {
|
@@ -25116,10 +25158,12 @@
|
|
25116
25158
|
if (contentType && contentType.indexOf('application/json') !== -1) {
|
25117
25159
|
return response.json().then((result) => {
|
25118
25160
|
if (result.error) {
|
25119
|
-
let error =
|
25161
|
+
let error = '<div style="font-family: monospace; font-size: 16px;">\n';
|
25162
|
+
error += `<div>${result.error}</div>\n`;
|
25120
25163
|
if (result.stacktrace) {
|
25121
|
-
error += '
|
25164
|
+
error += '<pre>\n' + result.stacktrace + '\n</pre>\n';
|
25122
25165
|
}
|
25166
|
+
error += '</div>\n';
|
25123
25167
|
document.getElementById('loading-box').innerHTML = error;
|
25124
25168
|
} else if (!result.server) {
|
25125
25169
|
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.49
|
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-28 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
|