sqlui 0.1.47 → 0.1.49
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.
- 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
|