sqlui 0.1.28 → 0.1.30
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 +10 -2
- data/app/deep.rb +20 -1
- data/app/server.rb +14 -11
- data/app/sqlui_config.rb +8 -16
- data/client/resources/sqlui.css +7 -1
- data/client/resources/sqlui.html +1 -0
- data/client/resources/sqlui.js +154 -63
- metadata +6 -35
- data/app/environment.rb +0 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a936a2e2476462ad9d92ed315bc02bee65b7c84171d5934e9728241f7a28ba1c
|
|
4
|
+
data.tar.gz: 2cf060fb96f672ebaf6b7fd9be602652e1a30841ded45c28415d7cc3dabc03e1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: addfac3cd62b75320dfcd2a437dac79db358b69391782f008e49be816241c1dad40d641accd078735c46de1d855eaf162956cc2a16370c297a72175e8b0bb7a2
|
|
7
|
+
data.tar.gz: 2a215084e23ac9b7c11d44f325fc27c77a597d3c9ed6f5e1ded238aa7223d82936f6d475674cb777406d491f4699f2a782cafabaf7c36d24be83d72e3eb998e8
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.30
|
data/app/args.rb
CHANGED
|
@@ -9,6 +9,10 @@ class Args
|
|
|
9
9
|
value
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
def self.fetch_non_empty_int(hash, key)
|
|
13
|
+
fetch_non_nil(hash, key, Integer)
|
|
14
|
+
end
|
|
15
|
+
|
|
12
16
|
def self.fetch_non_empty_hash(hash, key)
|
|
13
17
|
value = fetch_non_nil(hash, key, Hash)
|
|
14
18
|
raise ArgumentError, "required parameter #{key} empty" if value.empty?
|
|
@@ -23,9 +27,13 @@ class Args
|
|
|
23
27
|
raise ArgumentError, "required parameter #{key} null" if value.nil?
|
|
24
28
|
|
|
25
29
|
if classes.size.positive? && !classes.find { |clazz| value.is_a?(clazz) }
|
|
26
|
-
|
|
30
|
+
if classes.size != 1
|
|
31
|
+
raise ArgumentError, "required parameter #{key} not #{classes.map(&:to_s).map(&:downcase).join(' or ')}"
|
|
32
|
+
end
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
class_name = classes[0].to_s.downcase
|
|
35
|
+
class_name = %w[a e i o u].include?(class_name[0]) ? "an #{class_name}" : "a #{class_name}"
|
|
36
|
+
raise ArgumentError, "required parameter #{key} not #{class_name}"
|
|
29
37
|
end
|
|
30
38
|
|
|
31
39
|
value
|
data/app/deep.rb
CHANGED
|
@@ -7,6 +7,10 @@ module Enumerable
|
|
|
7
7
|
self
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
def deep_symbolize_keys!
|
|
11
|
+
deep_transform_keys!(&:to_sym)
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
def deep_dup(result = {})
|
|
11
15
|
map do |value|
|
|
12
16
|
value.respond_to?(:deep_dup) ? value.deep_dup : value.clone
|
|
@@ -18,11 +22,15 @@ end
|
|
|
18
22
|
# Deep extensions for Hash.
|
|
19
23
|
class Hash
|
|
20
24
|
def deep_transform_keys!(&block)
|
|
21
|
-
transform_keys!(
|
|
25
|
+
transform_keys!(&block)
|
|
22
26
|
each_value { |value| value.deep_transform_keys!(&block) if value.respond_to?(:deep_transform_keys!) }
|
|
23
27
|
self
|
|
24
28
|
end
|
|
25
29
|
|
|
30
|
+
def deep_symbolize_keys!
|
|
31
|
+
deep_transform_keys!(&:to_sym)
|
|
32
|
+
end
|
|
33
|
+
|
|
26
34
|
def deep_dup(result = {})
|
|
27
35
|
each do |key, value|
|
|
28
36
|
result[key] = value.respond_to?(:deep_dup) ? value.deep_dup : value.clone
|
|
@@ -55,4 +63,15 @@ class Hash
|
|
|
55
63
|
self.[](path[0]).deep_delete(*path[1..])
|
|
56
64
|
end
|
|
57
65
|
end
|
|
66
|
+
|
|
67
|
+
def deep_merge!(hash)
|
|
68
|
+
hash.each do |key, value|
|
|
69
|
+
if self[key].is_a?(Hash) && value.is_a?(Hash)
|
|
70
|
+
self[key].deep_merge!(value)
|
|
71
|
+
else
|
|
72
|
+
self[key] = value
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
self
|
|
76
|
+
end
|
|
58
77
|
end
|
data/app/server.rb
CHANGED
|
@@ -5,7 +5,6 @@ require 'json'
|
|
|
5
5
|
require 'sinatra/base'
|
|
6
6
|
require 'uri'
|
|
7
7
|
require_relative 'database_metadata'
|
|
8
|
-
require_relative 'environment'
|
|
9
8
|
require_relative 'mysql_types'
|
|
10
9
|
require_relative 'sql_parser'
|
|
11
10
|
require_relative 'sqlui'
|
|
@@ -15,8 +14,8 @@ class Server < Sinatra::Base
|
|
|
15
14
|
def self.init_and_run(config, resources_dir)
|
|
16
15
|
set :logging, true
|
|
17
16
|
set :bind, '0.0.0.0'
|
|
18
|
-
set :port,
|
|
19
|
-
set :
|
|
17
|
+
set :port, config.port
|
|
18
|
+
set :environment, config.environment
|
|
20
19
|
set :raise_errors, false
|
|
21
20
|
set :show_exceptions, false
|
|
22
21
|
|
|
@@ -41,18 +40,18 @@ class Server < Sinatra::Base
|
|
|
41
40
|
get "#{database.url_path}/sqlui.css" do
|
|
42
41
|
@css ||= File.read(File.join(resources_dir, 'sqlui.css'))
|
|
43
42
|
status 200
|
|
44
|
-
headers 'Content-Type'
|
|
43
|
+
headers 'Content-Type' => 'text/css'
|
|
45
44
|
body @css
|
|
46
45
|
end
|
|
47
46
|
|
|
48
47
|
get "#{database.url_path}/sqlui.js" do
|
|
49
48
|
@js ||= File.read(File.join(resources_dir, 'sqlui.js'))
|
|
50
49
|
status 200
|
|
51
|
-
headers 'Content-Type'
|
|
50
|
+
headers 'Content-Type' => 'text/javascript'
|
|
52
51
|
body @js
|
|
53
52
|
end
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
post "#{database.url_path}/metadata" do
|
|
56
55
|
metadata = database.with_client do |client|
|
|
57
56
|
{
|
|
58
57
|
server: "#{config.name} - #{database.display_name}",
|
|
@@ -77,7 +76,7 @@ class Server < Sinatra::Base
|
|
|
77
76
|
}
|
|
78
77
|
end
|
|
79
78
|
status 200
|
|
80
|
-
headers 'Content-Type'
|
|
79
|
+
headers 'Content-Type' => 'application/json'
|
|
81
80
|
body metadata.to_json
|
|
82
81
|
end
|
|
83
82
|
|
|
@@ -87,6 +86,7 @@ class Server < Sinatra::Base
|
|
|
87
86
|
|
|
88
87
|
full_sql = params[:sql]
|
|
89
88
|
sql = params[:sql]
|
|
89
|
+
variables = params[:variables] || {}
|
|
90
90
|
if params[:selection]
|
|
91
91
|
selection = params[:selection]
|
|
92
92
|
if selection.include?('-')
|
|
@@ -107,6 +107,9 @@ class Server < Sinatra::Base
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
result = database.with_client do |client|
|
|
110
|
+
variables.each do |name, value|
|
|
111
|
+
client.query("SET @#{name} = #{value};")
|
|
112
|
+
end
|
|
110
113
|
execute_query(client, sql)
|
|
111
114
|
end
|
|
112
115
|
|
|
@@ -114,21 +117,21 @@ class Server < Sinatra::Base
|
|
|
114
117
|
result[:query] = full_sql
|
|
115
118
|
|
|
116
119
|
status 200
|
|
117
|
-
headers 'Content-Type'
|
|
120
|
+
headers 'Content-Type' => 'application/json'
|
|
118
121
|
body result.to_json
|
|
119
122
|
end
|
|
120
123
|
|
|
121
124
|
get(%r{#{Regexp.escape(database.url_path)}/(query|graph|structure|saved)}) do
|
|
122
125
|
@html ||= File.read(File.join(resources_dir, 'sqlui.html'))
|
|
123
126
|
status 200
|
|
124
|
-
headers 'Content-Type'
|
|
127
|
+
headers 'Content-Type' => 'text/html'
|
|
125
128
|
body @html
|
|
126
129
|
end
|
|
127
130
|
end
|
|
128
131
|
|
|
129
132
|
error do |e|
|
|
130
133
|
status 500
|
|
131
|
-
headers 'Content-Type'
|
|
134
|
+
headers 'Content-Type' => 'application/json'
|
|
132
135
|
message = e.message.lines.first&.strip || 'unexpected error'
|
|
133
136
|
message = "#{message[0..80]}…" if message.length > 80
|
|
134
137
|
result = {
|
|
@@ -145,7 +148,7 @@ class Server < Sinatra::Base
|
|
|
145
148
|
|
|
146
149
|
def client_error(message, stacktrace: nil)
|
|
147
150
|
status(400)
|
|
148
|
-
headers
|
|
151
|
+
headers 'Content-Type' => 'application/json'
|
|
149
152
|
body({ error: message, stacktrace: stacktrace }.compact.to_json)
|
|
150
153
|
end
|
|
151
154
|
|
data/app/sqlui_config.rb
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'yaml'
|
|
4
|
-
require_relative 'database_config'
|
|
5
4
|
require_relative 'args'
|
|
5
|
+
require_relative 'database_config'
|
|
6
|
+
require_relative 'deep'
|
|
6
7
|
|
|
7
8
|
# App config including database configs.
|
|
8
9
|
class SqluiConfig
|
|
9
|
-
attr_reader :name, :list_url_path, :database_configs
|
|
10
|
+
attr_reader :name, :port, :environment, :list_url_path, :database_configs
|
|
10
11
|
|
|
11
|
-
def initialize(filename)
|
|
12
|
-
config = YAML.safe_load(ERB.new(File.read(filename)).result)
|
|
13
|
-
|
|
12
|
+
def initialize(filename, overrides = {})
|
|
13
|
+
config = YAML.safe_load(ERB.new(File.read(filename)).result).deep_merge!(overrides)
|
|
14
|
+
config.deep_symbolize_keys!
|
|
14
15
|
@name = Args.fetch_non_empty_string(config, :name).strip
|
|
16
|
+
@port = Args.fetch_non_empty_int(config, :port)
|
|
17
|
+
@environment = Args.fetch_non_empty_string(config, :environment).strip
|
|
15
18
|
@list_url_path = Args.fetch_non_empty_string(config, :list_url_path).strip
|
|
16
19
|
raise ArgumentError, 'list_url_path should start with a /' unless @list_url_path.start_with?('/')
|
|
17
20
|
if @list_url_path.length > 1 && @list_url_path.end_with?('/')
|
|
@@ -30,15 +33,4 @@ class SqluiConfig
|
|
|
30
33
|
|
|
31
34
|
config
|
|
32
35
|
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def deep_symbolize!(object)
|
|
37
|
-
return object unless object.is_a? Hash
|
|
38
|
-
|
|
39
|
-
object.transform_keys!(&:to_sym)
|
|
40
|
-
object.each_value { |child| deep_symbolize!(child) }
|
|
41
|
-
|
|
42
|
-
object
|
|
43
|
-
end
|
|
44
36
|
end
|
data/client/resources/sqlui.css
CHANGED
|
@@ -138,7 +138,7 @@ p {
|
|
|
138
138
|
font-family: Helvetica, sans-serif;
|
|
139
139
|
white-space: nowrap;
|
|
140
140
|
overflow: hidden;
|
|
141
|
-
font-size:
|
|
141
|
+
font-size: 16px;
|
|
142
142
|
color: #333;
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -322,3 +322,9 @@ select {
|
|
|
322
322
|
0% { transform: rotate(0deg); }
|
|
323
323
|
100% { transform: rotate(360deg); }
|
|
324
324
|
}
|
|
325
|
+
|
|
326
|
+
.result-time {
|
|
327
|
+
font-family: monospace;
|
|
328
|
+
color: #333;
|
|
329
|
+
font-size: 14px;
|
|
330
|
+
}
|
data/client/resources/sqlui.html
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
|
|
41
41
|
<div id="fetch-sql-box" class="fetch-sql-box tab-content-element graph-element query-element" style="display: none;">
|
|
42
42
|
<div id="result-loader" class="loader"></div>
|
|
43
|
+
<p id="result-time" class="result-time"></p>
|
|
43
44
|
</div>
|
|
44
45
|
|
|
45
46
|
<div id="saved-box" class="saved-box tab-content-element saved-element" style="display: none;">
|
data/client/resources/sqlui.js
CHANGED
|
@@ -23963,10 +23963,10 @@
|
|
|
23963
23963
|
function selectTab (event, tab) {
|
|
23964
23964
|
const url = new URL(window.location);
|
|
23965
23965
|
setTabInUrl(url, tab);
|
|
23966
|
-
route(event.target, event, url);
|
|
23966
|
+
route(event.target, event, url, true);
|
|
23967
23967
|
}
|
|
23968
23968
|
|
|
23969
|
-
function route (target = null, event = null, url = null) {
|
|
23969
|
+
function route (target = null, event = null, url = null, internal = false) {
|
|
23970
23970
|
if (url) {
|
|
23971
23971
|
if (event) {
|
|
23972
23972
|
event.preventDefault();
|
|
@@ -24004,10 +24004,10 @@
|
|
|
24004
24004
|
|
|
24005
24005
|
switch (window.tab) {
|
|
24006
24006
|
case 'query':
|
|
24007
|
-
selectResultTab();
|
|
24007
|
+
selectResultTab(internal);
|
|
24008
24008
|
break
|
|
24009
24009
|
case 'graph':
|
|
24010
|
-
selectGraphTab();
|
|
24010
|
+
selectGraphTab(internal);
|
|
24011
24011
|
break
|
|
24012
24012
|
case 'saved':
|
|
24013
24013
|
selectSavedTab();
|
|
@@ -24158,21 +24158,21 @@
|
|
|
24158
24158
|
});
|
|
24159
24159
|
}
|
|
24160
24160
|
|
|
24161
|
-
function selectGraphTab () {
|
|
24161
|
+
function selectGraphTab (internal) {
|
|
24162
24162
|
document.getElementById('query-box').style.display = 'flex';
|
|
24163
24163
|
document.getElementById('submit-box').style.display = 'flex';
|
|
24164
24164
|
document.getElementById('graph-box').style.display = 'flex';
|
|
24165
24165
|
document.getElementById('graph-status').style.display = 'flex';
|
|
24166
24166
|
document.getElementById('fetch-sql-box').style.display = 'none';
|
|
24167
24167
|
document.getElementById('cancel-button').style.display = 'none';
|
|
24168
|
-
maybeFetchResult();
|
|
24168
|
+
maybeFetchResult(internal);
|
|
24169
24169
|
|
|
24170
24170
|
const selection = getSelection();
|
|
24171
24171
|
focus();
|
|
24172
24172
|
setSelection(selection);
|
|
24173
24173
|
}
|
|
24174
24174
|
|
|
24175
|
-
function selectResultTab () {
|
|
24175
|
+
function selectResultTab (internal) {
|
|
24176
24176
|
document.getElementById('query-box').style.display = 'flex';
|
|
24177
24177
|
document.getElementById('submit-box').style.display = 'flex';
|
|
24178
24178
|
document.getElementById('result-box').style.display = 'flex';
|
|
@@ -24182,7 +24182,7 @@
|
|
|
24182
24182
|
const selection = getSelection();
|
|
24183
24183
|
focus();
|
|
24184
24184
|
setSelection(selection);
|
|
24185
|
-
maybeFetchResult();
|
|
24185
|
+
maybeFetchResult(internal);
|
|
24186
24186
|
}
|
|
24187
24187
|
|
|
24188
24188
|
function selectSavedTab () {
|
|
@@ -24212,7 +24212,7 @@
|
|
|
24212
24212
|
viewLinkElement.href = viewUrl.pathname + viewUrl.search;
|
|
24213
24213
|
viewLinkElement.addEventListener('click', function (event) {
|
|
24214
24214
|
clearResult();
|
|
24215
|
-
route(event.target, event, viewUrl);
|
|
24215
|
+
route(event.target, event, viewUrl, true);
|
|
24216
24216
|
});
|
|
24217
24217
|
|
|
24218
24218
|
const runUrl = new URL(window.location.origin + window.location.pathname);
|
|
@@ -24226,7 +24226,7 @@
|
|
|
24226
24226
|
runLinkElement.href = runUrl.pathname + runUrl.search;
|
|
24227
24227
|
runLinkElement.addEventListener('click', function (event) {
|
|
24228
24228
|
clearResult();
|
|
24229
|
-
route(event.target, event, runUrl);
|
|
24229
|
+
route(event.target, event, runUrl, true);
|
|
24230
24230
|
});
|
|
24231
24231
|
|
|
24232
24232
|
const nameElement = document.createElement('h2');
|
|
@@ -24296,20 +24296,21 @@
|
|
|
24296
24296
|
url.searchParams.delete('run');
|
|
24297
24297
|
}
|
|
24298
24298
|
|
|
24299
|
-
route(target, event, url);
|
|
24299
|
+
route(target, event, url, true);
|
|
24300
24300
|
}
|
|
24301
24301
|
|
|
24302
24302
|
function clearResult () {
|
|
24303
|
-
|
|
24304
|
-
|
|
24305
|
-
|
|
24306
|
-
|
|
24303
|
+
if (window.sqlFetch?.state === 'pending' || window.sqlFetch?.spinner === 'always') {
|
|
24304
|
+
window.sqlFetch.state = 'aborted';
|
|
24305
|
+
window.sqlFetch.spinner = 'never';
|
|
24306
|
+
window.sqlFetch.fetchController.abort();
|
|
24307
|
+
displaySqlFetch(window.sqlFetch);
|
|
24308
|
+
return
|
|
24307
24309
|
}
|
|
24308
24310
|
window.sqlFetch = null;
|
|
24309
|
-
|
|
24311
|
+
clearSpinner();
|
|
24310
24312
|
clearGraphBox();
|
|
24311
24313
|
clearGraphStatus();
|
|
24312
|
-
|
|
24313
24314
|
clearResultBox();
|
|
24314
24315
|
clearResultStatus();
|
|
24315
24316
|
}
|
|
@@ -24336,7 +24337,30 @@
|
|
|
24336
24337
|
}
|
|
24337
24338
|
}
|
|
24338
24339
|
|
|
24339
|
-
function
|
|
24340
|
+
function updateResultTime (sqlFetch) {
|
|
24341
|
+
if (window.sqlFetch === sqlFetch) {
|
|
24342
|
+
if (sqlFetch.state === 'pending' || sqlFetch.spinner === 'always') {
|
|
24343
|
+
displaySqlFetch(sqlFetch);
|
|
24344
|
+
setTimeout(() => { updateResultTime(sqlFetch); }, 500);
|
|
24345
|
+
}
|
|
24346
|
+
}
|
|
24347
|
+
}
|
|
24348
|
+
|
|
24349
|
+
function fetchSql (sqlFetch) {
|
|
24350
|
+
window.sqlFetch = sqlFetch;
|
|
24351
|
+
updateResultTime(sqlFetch);
|
|
24352
|
+
setTimeout(function () {
|
|
24353
|
+
if (window.sqlFetch === sqlFetch && sqlFetch.state === 'pending') {
|
|
24354
|
+
window.sqlFetch.spinner = 'always';
|
|
24355
|
+
displaySqlFetch(sqlFetch);
|
|
24356
|
+
setTimeout(function () {
|
|
24357
|
+
if (window.sqlFetch === sqlFetch) {
|
|
24358
|
+
window.sqlFetch.spinner = 'if_pending';
|
|
24359
|
+
displaySqlFetch(sqlFetch);
|
|
24360
|
+
}
|
|
24361
|
+
}, 400); // If we display a spinner, ensure it is displayed for at least 400 ms
|
|
24362
|
+
}
|
|
24363
|
+
}, 300); // Don't display the spinner unless the response takes more than 300 ms
|
|
24340
24364
|
fetch('query', {
|
|
24341
24365
|
headers: {
|
|
24342
24366
|
Accept: 'application/json',
|
|
@@ -24345,7 +24369,8 @@
|
|
|
24345
24369
|
method: 'POST',
|
|
24346
24370
|
body: JSON.stringify({
|
|
24347
24371
|
sql: sqlFetch.sql,
|
|
24348
|
-
selection
|
|
24372
|
+
selection: sqlFetch.selection,
|
|
24373
|
+
variables: sqlFetch.variables
|
|
24349
24374
|
}),
|
|
24350
24375
|
signal: sqlFetch.fetchController.signal
|
|
24351
24376
|
})
|
|
@@ -24368,14 +24393,14 @@
|
|
|
24368
24393
|
sqlFetch.error_message = 'failed to execute query';
|
|
24369
24394
|
}
|
|
24370
24395
|
}
|
|
24371
|
-
|
|
24396
|
+
displaySqlFetch(sqlFetch);
|
|
24372
24397
|
});
|
|
24373
24398
|
} else {
|
|
24374
24399
|
response.text().then((result) => {
|
|
24375
24400
|
sqlFetch.state = 'error';
|
|
24376
24401
|
sqlFetch.error_message = 'failed to execute query';
|
|
24377
24402
|
sqlFetch.error_details = result;
|
|
24378
|
-
|
|
24403
|
+
displaySqlFetch(sqlFetch);
|
|
24379
24404
|
});
|
|
24380
24405
|
}
|
|
24381
24406
|
})
|
|
@@ -24385,49 +24410,51 @@
|
|
|
24385
24410
|
sqlFetch.error_message = 'failed to execute query';
|
|
24386
24411
|
sqlFetch.error_details = error;
|
|
24387
24412
|
}
|
|
24388
|
-
|
|
24413
|
+
displaySqlFetch(sqlFetch);
|
|
24389
24414
|
});
|
|
24390
24415
|
}
|
|
24391
24416
|
|
|
24392
|
-
function
|
|
24417
|
+
function parseSqlVariables (params) {
|
|
24418
|
+
return Object.fromEntries(
|
|
24419
|
+
Array.from(params).filter(([key]) => {
|
|
24420
|
+
return key.match(/^_.+/)
|
|
24421
|
+
}).map(([key, value]) => {
|
|
24422
|
+
return [key.replace(/^_/, ''), value]
|
|
24423
|
+
})
|
|
24424
|
+
)
|
|
24425
|
+
}
|
|
24426
|
+
|
|
24427
|
+
function maybeFetchResult (internal) {
|
|
24393
24428
|
const url = new URL(window.location);
|
|
24394
24429
|
const params = url.searchParams;
|
|
24395
24430
|
const sql = params.get('sql');
|
|
24396
24431
|
const file = params.get('file');
|
|
24397
24432
|
const selection = params.get('selection');
|
|
24398
|
-
const
|
|
24433
|
+
const hasSqluiReferrer = document.referrer && new URL(document.referrer).origin === url.origin;
|
|
24434
|
+
const variables = parseSqlVariables(params);
|
|
24435
|
+
|
|
24436
|
+
// Only allow auto-run if coming from another SQLUI page. The idea here is to let the app link to URLs with run=true
|
|
24437
|
+
// but not other apps. This allows meta/shift-clicking to run a query.
|
|
24438
|
+
let run = false;
|
|
24439
|
+
if (params.has('run')) {
|
|
24440
|
+
run = (internal || hasSqluiReferrer) && ['1', 'true'].includes(params.get('run')?.toLowerCase());
|
|
24441
|
+
url.searchParams.delete('run');
|
|
24442
|
+
window.history.replaceState({}, '', url);
|
|
24443
|
+
}
|
|
24399
24444
|
|
|
24400
24445
|
if (params.has('file') && params.has('sql')) {
|
|
24401
24446
|
// TODO: show an error.
|
|
24402
24447
|
throw new Error('You can only specify a file or sql, not both.')
|
|
24403
24448
|
}
|
|
24404
24449
|
|
|
24405
|
-
const sqlFetch = {
|
|
24406
|
-
fetchController: new AbortController(),
|
|
24407
|
-
state: 'pending',
|
|
24408
|
-
sql,
|
|
24409
|
-
file,
|
|
24410
|
-
selection
|
|
24411
|
-
};
|
|
24412
|
-
|
|
24413
|
-
if (params.has('file')) {
|
|
24414
|
-
const fileDetails = window.metadata.saved[params.get('file')];
|
|
24415
|
-
if (!fileDetails) {
|
|
24416
|
-
throw new Error(`no such file: ${params.get('file')}`)
|
|
24417
|
-
}
|
|
24418
|
-
sqlFetch.file = file;
|
|
24419
|
-
sqlFetch.sql = fileDetails.contents;
|
|
24420
|
-
} else if (params.has('sql')) {
|
|
24421
|
-
sqlFetch.sql = sql;
|
|
24422
|
-
}
|
|
24423
|
-
|
|
24424
24450
|
const existingRequest = window.sqlFetch;
|
|
24425
24451
|
if (existingRequest) {
|
|
24426
24452
|
const selectionMatches = selection === existingRequest.selection;
|
|
24427
24453
|
const sqlMatches = params.has('sql') && sql === existingRequest.sql;
|
|
24428
24454
|
const fileMatches = params.has('file') && file === existingRequest.file;
|
|
24455
|
+
const variablesMatch = JSON.stringify(variables) === JSON.stringify(existingRequest.variables);
|
|
24429
24456
|
const queryMatches = sqlMatches || fileMatches;
|
|
24430
|
-
if (selectionMatches && queryMatches) {
|
|
24457
|
+
if (selectionMatches && queryMatches && variablesMatch) {
|
|
24431
24458
|
displaySqlFetch(existingRequest);
|
|
24432
24459
|
if (params.has('selection')) {
|
|
24433
24460
|
focus();
|
|
@@ -24439,14 +24466,11 @@
|
|
|
24439
24466
|
|
|
24440
24467
|
clearResult();
|
|
24441
24468
|
|
|
24469
|
+
const sqlFetch = buildSqlFetch(sql, file, variables, selection);
|
|
24442
24470
|
if (params.has('sql') || params.has('file')) {
|
|
24443
24471
|
setValue(sqlFetch.sql);
|
|
24444
24472
|
if (run) {
|
|
24445
|
-
|
|
24446
|
-
window.history.replaceState({}, '', url);
|
|
24447
|
-
window.sqlFetch = sqlFetch;
|
|
24448
|
-
displaySqlFetch(sqlFetch);
|
|
24449
|
-
fetchSql(sqlFetch, selection, displaySqlFetch);
|
|
24473
|
+
fetchSql(sqlFetch);
|
|
24450
24474
|
}
|
|
24451
24475
|
}
|
|
24452
24476
|
if (params.has('selection')) {
|
|
@@ -24455,12 +24479,43 @@
|
|
|
24455
24479
|
}
|
|
24456
24480
|
}
|
|
24457
24481
|
|
|
24482
|
+
function buildSqlFetch (sql, file, variables, selection) {
|
|
24483
|
+
const sqlFetch = {
|
|
24484
|
+
fetchController: new AbortController(),
|
|
24485
|
+
state: 'pending',
|
|
24486
|
+
startedAt: window.performance.now(),
|
|
24487
|
+
spinner: 'never',
|
|
24488
|
+
finished: null,
|
|
24489
|
+
sql,
|
|
24490
|
+
file,
|
|
24491
|
+
selection,
|
|
24492
|
+
variables
|
|
24493
|
+
};
|
|
24494
|
+
|
|
24495
|
+
if (file) {
|
|
24496
|
+
const fileDetails = window.metadata.saved[file];
|
|
24497
|
+
if (!fileDetails) {
|
|
24498
|
+
throw new Error(`no such file: ${file}`)
|
|
24499
|
+
}
|
|
24500
|
+
sqlFetch.file = file;
|
|
24501
|
+
sqlFetch.sql = fileDetails.contents;
|
|
24502
|
+
} else if (sql) {
|
|
24503
|
+
sqlFetch.sql = sql;
|
|
24504
|
+
}
|
|
24505
|
+
|
|
24506
|
+
return sqlFetch
|
|
24507
|
+
}
|
|
24508
|
+
|
|
24458
24509
|
function displaySqlFetchInResultTab (fetch) {
|
|
24459
|
-
if (fetch.state === 'pending') {
|
|
24510
|
+
if (fetch.state === 'pending' || fetch.spinner === 'always') {
|
|
24460
24511
|
clearResultBox();
|
|
24461
|
-
|
|
24462
|
-
|
|
24463
|
-
|
|
24512
|
+
if (fetch.spinner === 'never') {
|
|
24513
|
+
document.getElementById('result-box').style.display = 'flex';
|
|
24514
|
+
clearSpinner();
|
|
24515
|
+
} else {
|
|
24516
|
+
document.getElementById('result-box').style.display = 'none';
|
|
24517
|
+
displaySpinner(fetch);
|
|
24518
|
+
}
|
|
24464
24519
|
return
|
|
24465
24520
|
}
|
|
24466
24521
|
|
|
@@ -24470,6 +24525,7 @@
|
|
|
24470
24525
|
|
|
24471
24526
|
if (fetch.state === 'aborted') {
|
|
24472
24527
|
clearResultBox();
|
|
24528
|
+
document.getElementById('result-status').innerText = 'query cancelled';
|
|
24473
24529
|
return
|
|
24474
24530
|
}
|
|
24475
24531
|
|
|
@@ -24489,7 +24545,7 @@
|
|
|
24489
24545
|
}
|
|
24490
24546
|
|
|
24491
24547
|
clearResultBox();
|
|
24492
|
-
displaySqlFetchResultStatus('result-status', fetch
|
|
24548
|
+
displaySqlFetchResultStatus('result-status', fetch);
|
|
24493
24549
|
|
|
24494
24550
|
const tableElement = document.createElement('table');
|
|
24495
24551
|
tableElement.id = 'result-table';
|
|
@@ -24544,12 +24600,44 @@
|
|
|
24544
24600
|
}
|
|
24545
24601
|
}
|
|
24546
24602
|
|
|
24603
|
+
function clearSpinner () {
|
|
24604
|
+
document.getElementById('cancel-button').style.display = 'none';
|
|
24605
|
+
document.getElementById('fetch-sql-box').style.display = 'none';
|
|
24606
|
+
}
|
|
24607
|
+
|
|
24608
|
+
function displaySpinner (fetch) {
|
|
24609
|
+
document.getElementById('cancel-button').style.display = 'flex';
|
|
24610
|
+
document.getElementById('fetch-sql-box').style.display = 'flex';
|
|
24611
|
+
|
|
24612
|
+
const elapsed = window.performance.now() - fetch.startedAt;
|
|
24613
|
+
const seconds = Math.floor((elapsed / 1000) % 60);
|
|
24614
|
+
const minutes = Math.floor((elapsed / 1000 / 60) % 60);
|
|
24615
|
+
let display = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
24616
|
+
if (elapsed >= 1000 * 60 * 60) {
|
|
24617
|
+
const hours = Math.floor((elapsed / 1000 / 60 / 60) % 24);
|
|
24618
|
+
display = `${hours.toString().padStart(2, '0')}:${display}`;
|
|
24619
|
+
if (elapsed >= 1000 * 60 * 60 * 24) {
|
|
24620
|
+
const days = Math.floor(elapsed / 1000 / 60 / 60 / 24);
|
|
24621
|
+
if (days === 1) {
|
|
24622
|
+
display = `${days} day ${display}`;
|
|
24623
|
+
} else {
|
|
24624
|
+
display = `${days.toLocaleString()} days ${display}`;
|
|
24625
|
+
}
|
|
24626
|
+
}
|
|
24627
|
+
}
|
|
24628
|
+
document.getElementById('result-time').innerText = display;
|
|
24629
|
+
}
|
|
24630
|
+
|
|
24547
24631
|
function displaySqlFetchInGraphTab (fetch) {
|
|
24548
|
-
if (fetch.state === 'pending') {
|
|
24632
|
+
if (fetch.state === 'pending' || fetch.spinner === 'always') {
|
|
24549
24633
|
clearGraphBox();
|
|
24550
|
-
|
|
24551
|
-
|
|
24552
|
-
|
|
24634
|
+
if (fetch.spinner === 'never') {
|
|
24635
|
+
document.getElementById('graph-box').style.display = 'flex';
|
|
24636
|
+
clearSpinner();
|
|
24637
|
+
} else {
|
|
24638
|
+
document.getElementById('graph-box').style.display = 'none';
|
|
24639
|
+
displaySpinner(fetch);
|
|
24640
|
+
}
|
|
24553
24641
|
return
|
|
24554
24642
|
}
|
|
24555
24643
|
|
|
@@ -24559,6 +24647,7 @@
|
|
|
24559
24647
|
|
|
24560
24648
|
if (fetch.state === 'aborted') {
|
|
24561
24649
|
clearGraphBox();
|
|
24650
|
+
document.getElementById('graph-status').innerText = 'query cancelled';
|
|
24562
24651
|
return
|
|
24563
24652
|
}
|
|
24564
24653
|
|
|
@@ -24572,7 +24661,7 @@
|
|
|
24572
24661
|
throw new Error(`unexpected fetch sql request status: ${fetch.status}`)
|
|
24573
24662
|
}
|
|
24574
24663
|
clearGraphBox();
|
|
24575
|
-
displaySqlFetchResultStatus('graph-status', fetch
|
|
24664
|
+
displaySqlFetchResultStatus('graph-status', fetch);
|
|
24576
24665
|
|
|
24577
24666
|
if (!fetch.result.rows) {
|
|
24578
24667
|
return
|
|
@@ -24611,13 +24700,15 @@
|
|
|
24611
24700
|
chart.draw(dataTable, options);
|
|
24612
24701
|
}
|
|
24613
24702
|
|
|
24614
|
-
function displaySqlFetchResultStatus (statusElementId,
|
|
24703
|
+
function displaySqlFetchResultStatus (statusElementId, sqlFetch) {
|
|
24704
|
+
const result = sqlFetch.result;
|
|
24615
24705
|
const statusElement = document.getElementById(statusElementId);
|
|
24706
|
+
const elapsed = Math.round(100 * (window.performance.now() - sqlFetch.startedAt) / 1000.0) / 100;
|
|
24616
24707
|
|
|
24617
24708
|
if (result.total_rows === 1) {
|
|
24618
|
-
statusElement.innerText = `${result.total_rows} row`;
|
|
24709
|
+
statusElement.innerText = `${result.total_rows} row (${elapsed}s)`;
|
|
24619
24710
|
} else {
|
|
24620
|
-
statusElement.innerText = `${result.total_rows} rows`;
|
|
24711
|
+
statusElement.innerText = `${result.total_rows} rows (${elapsed}s)`;
|
|
24621
24712
|
}
|
|
24622
24713
|
|
|
24623
24714
|
if (result.total_rows > result.rows.length) {
|
|
@@ -24647,7 +24738,7 @@
|
|
|
24647
24738
|
headers: {
|
|
24648
24739
|
Accept: 'application/json'
|
|
24649
24740
|
},
|
|
24650
|
-
method: '
|
|
24741
|
+
method: 'POST'
|
|
24651
24742
|
})
|
|
24652
24743
|
])
|
|
24653
24744
|
.then((results) => {
|
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.30
|
|
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-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: mysql2
|
|
@@ -24,20 +24,6 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0.0'
|
|
27
|
-
- !ruby/object:Gem::Dependency
|
|
28
|
-
name: puma
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - "~>"
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '6.0'
|
|
34
|
-
type: :runtime
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - "~>"
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '6.0'
|
|
41
27
|
- !ruby/object:Gem::Dependency
|
|
42
28
|
name: sinatra
|
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -53,19 +39,19 @@ dependencies:
|
|
|
53
39
|
- !ruby/object:Gem::Version
|
|
54
40
|
version: '3.0'
|
|
55
41
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
42
|
+
name: webrick
|
|
57
43
|
requirement: !ruby/object:Gem::Requirement
|
|
58
44
|
requirements:
|
|
59
45
|
- - "~>"
|
|
60
46
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
62
|
-
type: :
|
|
47
|
+
version: '1.0'
|
|
48
|
+
type: :runtime
|
|
63
49
|
prerelease: false
|
|
64
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
51
|
requirements:
|
|
66
52
|
- - "~>"
|
|
67
53
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
54
|
+
version: '1.0'
|
|
69
55
|
- !ruby/object:Gem::Dependency
|
|
70
56
|
name: rspec-core
|
|
71
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -136,20 +122,6 @@ dependencies:
|
|
|
136
122
|
- - "~>"
|
|
137
123
|
- !ruby/object:Gem::Version
|
|
138
124
|
version: '4.0'
|
|
139
|
-
- !ruby/object:Gem::Dependency
|
|
140
|
-
name: watir
|
|
141
|
-
requirement: !ruby/object:Gem::Requirement
|
|
142
|
-
requirements:
|
|
143
|
-
- - "~>"
|
|
144
|
-
- !ruby/object:Gem::Version
|
|
145
|
-
version: '7.0'
|
|
146
|
-
type: :development
|
|
147
|
-
prerelease: false
|
|
148
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
-
requirements:
|
|
150
|
-
- - "~>"
|
|
151
|
-
- !ruby/object:Gem::Version
|
|
152
|
-
version: '7.0'
|
|
153
125
|
description: A SQL UI.
|
|
154
126
|
email: nicholasdower@gmail.com
|
|
155
127
|
executables:
|
|
@@ -162,7 +134,6 @@ files:
|
|
|
162
134
|
- app/database_config.rb
|
|
163
135
|
- app/database_metadata.rb
|
|
164
136
|
- app/deep.rb
|
|
165
|
-
- app/environment.rb
|
|
166
137
|
- app/mysql_types.rb
|
|
167
138
|
- app/server.rb
|
|
168
139
|
- app/sql_parser.rb
|
data/app/environment.rb
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Parses and provides access to environment variables.
|
|
4
|
-
class Environment
|
|
5
|
-
APP_ENV = ENV.fetch('APP_ENV', 'development').to_sym
|
|
6
|
-
APP_PORT = ENV.fetch('APP_PORT', 8080)
|
|
7
|
-
|
|
8
|
-
def self.server_env
|
|
9
|
-
APP_ENV
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def self.development?
|
|
13
|
-
APP_ENV == :development
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def self.production?
|
|
17
|
-
APP_ENV == :production
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def self.server_port
|
|
21
|
-
APP_PORT
|
|
22
|
-
end
|
|
23
|
-
end
|