sqlui 0.1.24 → 0.1.25

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 496f67632ee9516c9569403f9907c96e9efad644cdba583fbc55b2112cfd2eb7
4
- data.tar.gz: d186c82b1fb261ec365275412319c19e61acfe117472f0e370b541be3fb13af1
3
+ metadata.gz: 11963565427ad327b2fb3cf1eadb62f789b620c3b3de943f2a0b17f3be505356
4
+ data.tar.gz: 1be9eaac08e81afba77f4c1f5581a78f8e4a73d007d9e29f0ca238c956d46a6b
5
5
  SHA512:
6
- metadata.gz: e9d3a25232cbdb23eaec1672da41de0825defc0fb32fb2a3d3f1cc4a639b8d6945c6ff5f8ee8165f815a34ccb61f22443bc6553e840fdd6a9ecfa74b7181754d
7
- data.tar.gz: e165ae26e57800866243a266957b8d9e40da901589c8c0f44efa5efc9ac0ffb293bfd096ecaabca58c972ddfb8ac1b33e9518b9cbef60cd0233d3c97082b36ac
6
+ metadata.gz: 76fc95eeec60f207e1505b4d71d10e6bf329c94ddc42ca15edef83853ca6f74407b275f168f1a721c9b0d50507228dcb5aae5c4f2cb2fa54e46f1dbef79d05cb
7
+ data.tar.gz: 06140f3797fb34a7fa869c484ef002e5310ceceeea87e57f0c6dd984f13c2a6d75c9dddab253bf64545fa9cd58a93cac41e476b8cb8ee9c7fe86f47b38af39a0
data/.version CHANGED
@@ -1 +1 @@
1
- 0.1.24
1
+ 0.1.25
data/app/server.rb CHANGED
@@ -25,20 +25,17 @@ class Server < Sinatra::Base
25
25
  body 'OK'
26
26
  end
27
27
 
28
+ get '/?' do
29
+ redirect config.list_url_path, 301
30
+ end
31
+
28
32
  get "#{config.list_url_path}/?" do
29
33
  erb :databases, locals: { config: config }
30
34
  end
31
35
 
32
36
  config.database_configs.each do |database|
33
- get database.url_path.to_s do
34
- redirect "#{database.url_path}/app", 301
35
- end
36
-
37
- get "#{database.url_path}/app" do
38
- @html ||= File.read(File.join(resources_dir, 'sqlui.html'))
39
- status 200
40
- headers 'Content-Type': 'text/html'
41
- body @html
37
+ get "#{database.url_path}/?" do
38
+ redirect "#{database.url_path}/query", 301
42
39
  end
43
40
 
44
41
  get "#{database.url_path}/sqlui.css" do
@@ -59,16 +56,23 @@ class Server < Sinatra::Base
59
56
  metadata = database.with_client do |client|
60
57
  {
61
58
  server: "#{config.name} - #{database.display_name}",
59
+ list_url_path: config.list_url_path,
62
60
  schemas: DatabaseMetadata.lookup(client, database),
63
- saved: Dir.glob("#{database.saved_path}/*.sql").map do |path|
64
- comment_lines = File.readlines(path).take_while do |l|
61
+ saved: Dir.glob("#{database.saved_path}/*.sql").to_h do |path|
62
+ contents = File.read(path)
63
+ comment_lines = contents.split("\n").take_while do |l|
65
64
  l.start_with?('--')
66
65
  end
66
+ filename = File.basename(path)
67
67
  description = comment_lines.map { |l| l.sub(/^-- */, '') }.join
68
- {
69
- filename: File.basename(path),
70
- description: description
71
- }
68
+ [
69
+ filename,
70
+ {
71
+ filename: filename,
72
+ description: description,
73
+ contents: contents.strip
74
+ }
75
+ ]
72
76
  end
73
77
  }
74
78
  end
@@ -77,36 +81,28 @@ class Server < Sinatra::Base
77
81
  body metadata.to_json
78
82
  end
79
83
 
80
- get "#{database.url_path}/query_file" do
81
- break client_error('missing file param') unless params[:file]
82
-
83
- file = File.join(database.saved_path, params[:file])
84
- break client_error('no such file') unless File.exist?(file)
85
-
86
- sql = File.read(file)
87
- result = database.with_client do |client|
88
- execute_query(client, sql).tap { |r| r[:file] = params[:file] }
89
- end
90
-
91
- status 200
92
- headers 'Content-Type': 'application/json'
93
- body result.to_json
94
- end
95
-
96
84
  post "#{database.url_path}/query" do
97
85
  params.merge!(JSON.parse(request.body.read, symbolize_names: true))
98
86
  break client_error('missing sql') unless params[:sql]
99
87
 
88
+ full_sql = params[:sql]
100
89
  sql = params[:sql]
101
- selection = params[:selection]
102
- if selection
103
- selection = selection.split(':').map { |v| Integer(v) }
90
+ if params[:selection]
91
+ selection = params[:selection]
92
+ if selection.include?('-')
93
+ # sort because the selection could be in either direction
94
+ selection = params[:selection].split('-').map { |v| Integer(v) }.sort
95
+ else
96
+ selection = Integer(selection)
97
+ selection = [selection, selection]
98
+ end
104
99
 
105
100
  sql = if selection[0] == selection[1]
106
101
  SqlParser.find_statement_at_cursor(params[:sql], selection[0])
107
102
  else
108
- params[:sql][selection[0], selection[1]]
103
+ full_sql[selection[0], selection[1]]
109
104
  end
105
+
110
106
  break client_error("can't find query at selection") unless sql
111
107
  end
112
108
 
@@ -115,11 +111,19 @@ class Server < Sinatra::Base
115
111
  end
116
112
 
117
113
  result[:selection] = params[:selection]
114
+ result[:query] = full_sql
118
115
 
119
116
  status 200
120
117
  headers 'Content-Type': 'application/json'
121
118
  body result.to_json
122
119
  end
120
+
121
+ get(%r{#{Regexp.escape(database.url_path)}/(query|graph|structure|saved)}) do
122
+ @html ||= File.read(File.join(resources_dir, 'sqlui.html'))
123
+ status 200
124
+ headers 'Content-Type': 'text/html'
125
+ body @html
126
+ end
123
127
  end
124
128
 
125
129
  error do |e|
@@ -165,7 +169,6 @@ class Server < Sinatra::Base
165
169
  columns = []
166
170
  end
167
171
  {
168
- query: sql,
169
172
  columns: columns,
170
173
  column_types: column_types,
171
174
  total_rows: rows.size,
@@ -1,11 +1,11 @@
1
- <html>
1
+ <html lang="en">
2
2
  <head>
3
3
  <title>Databases</title>
4
4
 
5
5
  <style>
6
6
  body {
7
- font-family: Helvetica;
8
- margin: 0px;
7
+ font-family: Helvetica, sans-serif;
8
+ margin: 0;
9
9
  }
10
10
 
11
11
  h1 {
@@ -25,7 +25,8 @@
25
25
  flex-direction: row;
26
26
  border-bottom: 1px solid #ddd;
27
27
  height: 36px;
28
- font-family: Helvetica;
28
+ font-family: Helvetica, sans-serif;
29
+ padding: 5px;
29
30
  }
30
31
 
31
32
  .header, .server-name {
@@ -41,28 +42,34 @@
41
42
  font-weight: normal;
42
43
  }
43
44
 
45
+ .name-and-links {
46
+ display: flex;
47
+ flex-direction: row;
48
+ align-items: center;
49
+ }
50
+
44
51
  .header {
45
52
  font-weight: bold;
46
53
  padding-left: 5px;
47
54
  }
48
55
 
49
56
  .database a {
50
- margin-right: 10px;
57
+ margin-right: 5px;
51
58
  color: darkblue;
52
- font-size: 16px
59
+ font-size: 16px;
60
+ text-decoration: none;
53
61
  }
54
62
 
55
63
  .database h2 {
56
- margin: 0px;
64
+ margin: 0 10px 0 0;
57
65
  }
58
66
 
59
67
  .database p {
60
- margin: 0px;
61
- margin-top: 10px;
68
+ margin: 10px 0 0;
62
69
  }
63
70
 
64
71
  .database {
65
- margin: 0px;
72
+ margin: 0;
66
73
  padding: 10px;
67
74
  cursor: pointer;
68
75
  border-bottom: 1px solid #eeeeee;
@@ -76,6 +83,18 @@
76
83
  background: #eee;
77
84
  }
78
85
  </style>
86
+
87
+ <script>
88
+ function openDatabase(event, url) {
89
+ if (event.shiftKey) {
90
+ window.open(url, '_blank').focus()
91
+ } else if (event.metaKey) {
92
+ window.open(url).focus()
93
+ } else {
94
+ window.location = url
95
+ }
96
+ }
97
+ </script>
79
98
  </head>
80
99
 
81
100
  <body>
@@ -84,11 +103,13 @@
84
103
  <h1 class="server-name"><%= config.name %> Databases</h1>
85
104
  </div>
86
105
  <% config.database_configs.each do |database_config| %>
87
- <div class="database" onclick="window.location='<%= "#{database_config.url_path}/app" %>'">
88
- <h2 class='name'><%= database_config.display_name %></h2>
89
- <a class='query-link' href="<%= database_config.url_path %>/app">query</a>
90
- <a class='saved-link' href="<%= database_config.url_path %>/app?tab=saved">saved</a>
91
- <a class='structure-link' href="<%= database_config.url_path %>/app?tab=structure">structure</a>
106
+ <div class="database" onclick="openDatabase(event, '<%= "#{database_config.url_path}/query" %>')">
107
+ <div class="name-and-links">
108
+ <h2 class='name'><%= database_config.display_name %></h2>
109
+ <a class='query-link' href="<%= database_config.url_path %>/query" onclick="event.stopPropagation()">query</a>
110
+ <a class='saved-link' href="<%= database_config.url_path %>/saved" onclick="event.stopPropagation()">saved</a>
111
+ <a class='structure-link' href="<%= database_config.url_path %>/structure" onclick="event.stopPropagation()">structure</a>
112
+ </div>
92
113
  <p class='description'>
93
114
  <%= database_config.description %>
94
115
  </p>
@@ -71,6 +71,7 @@ p {
71
71
  .tab-button, .selected-tab-button {
72
72
  border: none;
73
73
  outline: none;
74
+ text-decoration: none;
74
75
  cursor: pointer;
75
76
  width: 150px;
76
77
  padding: 2px;
@@ -79,6 +80,7 @@ p {
79
80
  justify-content: center;
80
81
  background-color: #fff;
81
82
  font-size: 18px;
83
+ align-self: center;
82
84
  }
83
85
 
84
86
  .tab-button {
@@ -110,6 +112,7 @@ p {
110
112
  .submit-box {
111
113
  display: flex;
112
114
  border-top: 1px solid #ddd;
115
+ border-bottom: 1px solid #ddd;
113
116
  justify-content: right;
114
117
  }
115
118
 
@@ -135,15 +138,16 @@ p {
135
138
  color: #333;
136
139
  }
137
140
 
138
- .result-box, .saved-box, .graph-box, .structure-box {
141
+ .result-box, .fetch-sql-box, .saved-box, .graph-box, .structure-box {
139
142
  flex: 1;
140
143
  overflow: auto;
141
144
  display: flex;
142
145
  flex-direction: column;
143
146
  }
144
147
 
145
- .graph-box, .result-box {
146
- border-top: 1px solid #ddd;
148
+ .fetch-sql-box {
149
+ justify-content: center;
150
+ align-items: center;
147
151
  }
148
152
 
149
153
  .graph-box {
@@ -218,6 +222,7 @@ thead {
218
222
 
219
223
  .tabs-box {
220
224
  display: flex;
225
+ padding: 5px;
221
226
  }
222
227
 
223
228
  .saved-box {
@@ -287,3 +292,17 @@ select {
287
292
  color: #333;
288
293
  font-size: 18px;
289
294
  }
295
+
296
+ .loader {
297
+ border: 8px solid #eee; /* Light grey */
298
+ border-top: 8px solid #888; /* Blue */
299
+ border-radius: 50%;
300
+ width: 40px;
301
+ height: 40px;
302
+ animation: spin 2s linear infinite;
303
+ }
304
+
305
+ @keyframes spin {
306
+ 0% { transform: rotate(0deg); }
307
+ 100% { transform: rotate(360deg); }
308
+ }
@@ -13,12 +13,12 @@
13
13
 
14
14
  <div id="main-box" class="main-box" style="display:none">
15
15
  <div class="tabs-box">
16
- <h1 class="header"><a href="/sqlui">SQLUI</a></h1>
16
+ <h1 class="header"><a id="header-link">SQLUI</a></h1>
17
17
  <h1 id="server-name" class="server-name"></h1>
18
- <input id="query-tab-button" class="tab-button" type="button" value="Query" onclick="sqlui.selectTab('query')"></input>
19
- <input id="graph-tab-button" class="tab-button" type="button" value="Graph" onclick="sqlui.selectTab('graph')"></input>
20
- <input id="saved-tab-button" class="tab-button" type="button" value="Saved" onclick="sqlui.selectTab('saved')"></input>
21
- <input id="structure-tab-button" class="tab-button" type="button" value="Structure" onclick="sqlui.selectTab('structure')"></input>
18
+ <a id="query-tab-button" class="tab-button">Query</a>
19
+ <a id="graph-tab-button" class="tab-button">Graph</a>
20
+ <a id="saved-tab-button" class="tab-button">Saved</a>
21
+ <a id="structure-tab-button" class="tab-button">Structure</a>
22
22
  </div>
23
23
 
24
24
  <div id="query-box" class="query-box tab-content-element graph-element query-element" style="display: none;">
@@ -26,8 +26,8 @@
26
26
  </div>
27
27
 
28
28
  <div id="submit-box" class="submit-box tab-content-element graph-element query-element" style="display: none;">
29
- <input id="submit-all-button" class="submit-button" type="button" value="execute (ctrl-shift-enter)" onclick="sqlui.submitAll();"></input>
30
- <input id="submit-current-button" class="submit-button" type="button" value="execute selection (ctrl-enter)" onclick="sqlui.submitCurrent();"></input>
29
+ <input id="submit-current-button" class="submit-button" type="button" value="run selection (ctrl-enter)"></input>
30
+ <input id="submit-all-button" class="submit-button" type="button" value="run (ctrl-shift-enter)"></input>
31
31
  </div>
32
32
 
33
33
  <div id="result-box" class="result-box tab-content-element query-element" style="display: none;">
@@ -36,6 +36,10 @@
36
36
  <div id="graph-box" class="graph-box tab-content-element graph-element" style="display: none;">
37
37
  </div>
38
38
 
39
+ <div id="fetch-sql-box" class="fetch-sql-box tab-content-element graph-element query-element" style="display: none;">
40
+ <div id="result-loader" class="loader"></div>
41
+ </div>
42
+
39
43
  <div id="saved-box" class="saved-box tab-content-element saved-element" style="display: none;">
40
44
  </div>
41
45
 
@@ -55,7 +59,7 @@
55
59
  </div>
56
60
 
57
61
  <div id="status-box" class="status-box">
58
- <div id="query-status" class="status tab-content-element query-element"></div>
62
+ <div id="result-status" class="status tab-content-element query-element"></div>
59
63
  <div id="graph-status" class="status tab-content-element graph-element"></div>
60
64
  <div id="saved-status" class="status tab-content-element saved-element"></div>
61
65
  </div>
@@ -1,4 +1,4 @@
1
- var sqlui = (function (exports) {
1
+ (function () {
2
2
  'use strict';
3
3
 
4
4
  /**
@@ -23851,6 +23851,25 @@ var sqlui = (function (exports) {
23851
23851
  /* global google */
23852
23852
 
23853
23853
  function init (parent, onSubmit, onShiftSubmit) {
23854
+ document.getElementById('query-tab-button').addEventListener('click', function (event) {
23855
+ selectTab(event, 'query');
23856
+ });
23857
+ document.getElementById('saved-tab-button').addEventListener('click', function (event) {
23858
+ selectTab(event, 'saved');
23859
+ });
23860
+ document.getElementById('structure-tab-button').addEventListener('click', function (event) {
23861
+ selectTab(event, 'structure');
23862
+ });
23863
+ document.getElementById('graph-tab-button').addEventListener('click', function (event) {
23864
+ selectTab(event, 'graph');
23865
+ });
23866
+ document.getElementById('submit-all-button').addEventListener('click', function (event) {
23867
+ submitAll(event.target, event);
23868
+ });
23869
+ document.getElementById('submit-current-button').addEventListener('click', function (event) {
23870
+ submitCurrent(event.target, event);
23871
+ });
23872
+
23854
23873
  const fixedHeightEditor = EditorView.theme({
23855
23874
  '.cm-scroller': { height: '200px', overflow: 'auto', resize: 'vertical' }
23856
23875
  });
@@ -23872,18 +23891,27 @@ var sqlui = (function (exports) {
23872
23891
  }
23873
23892
 
23874
23893
  function getSelection () {
23875
- return [window.editorView.state.selection.main.from, window.editorView.state.selection.main.to]
23894
+ const anchor = window.editorView.state.selection.main.anchor;
23895
+ const head = window.editorView.state.selection.main.head;
23896
+ if (anchor === head) {
23897
+ return `${anchor}`
23898
+ } else {
23899
+ return `${anchor}-${head}`
23900
+ }
23876
23901
  }
23877
23902
 
23878
23903
  function setSelection (selection) {
23879
- window.editorView.dispatch(
23880
- {
23881
- selection: {
23882
- anchor: Math.min(selection[0], window.editorView.state.doc.length),
23883
- head: Math.min(selection[1], window.editorView.state.doc.length)
23884
- }
23885
- }
23886
- );
23904
+ let anchor;
23905
+ let head;
23906
+ if (selection.includes('-')) {
23907
+ selection = selection.split('-').map(x => parseInt(x));
23908
+ anchor = Math.min(selection[0], window.editorView.state.doc.length);
23909
+ head = Math.min(selection[1], window.editorView.state.doc.length);
23910
+ } else {
23911
+ anchor = Math.min(parseInt(selection), window.editorView.state.doc.length);
23912
+ head = anchor;
23913
+ }
23914
+ window.editorView.dispatch({ selection: { anchor, head } });
23887
23915
  }
23888
23916
 
23889
23917
  function focus () {
@@ -23904,30 +23932,62 @@ var sqlui = (function (exports) {
23904
23932
  });
23905
23933
  }
23906
23934
 
23907
- function selectTab (tab) {
23908
- window.tab = tab;
23935
+ function setTabInUrl (url, tab) {
23936
+ url.pathname = url.pathname.replace(/\/[^/]+$/, `/${tab}`);
23937
+ }
23938
+
23939
+ function getTabFromUrl (url) {
23940
+ const match = url.pathname.match(/\/([^/]+)$/);
23941
+ if (match && ['query', 'graph', 'structure', 'saved'].includes(match[1])) {
23942
+ return match[1]
23943
+ } else {
23944
+ throw new Error(`invalid tab: ${url.pathname}`)
23945
+ }
23946
+ }
23947
+
23948
+ function updateTabs () {
23909
23949
  const url = new URL(window.location);
23910
- if (url.searchParams.has('tab')) {
23911
- if (url.searchParams.get('tab') !== tab) {
23912
- if (tab === 'query') {
23913
- url.searchParams.delete('tab');
23914
- window.history.pushState({}, '', url);
23915
- return route()
23916
- } else {
23917
- url.searchParams.set('tab', tab);
23918
- window.history.pushState({}, '', url);
23919
- return route()
23950
+ setTabInUrl(url, 'graph');
23951
+ document.getElementById('graph-tab-button').href = url.pathname + url.search;
23952
+ setTabInUrl(url, 'saved');
23953
+ document.getElementById('saved-tab-button').href = url.pathname + url.search;
23954
+ setTabInUrl(url, 'structure');
23955
+ document.getElementById('structure-tab-button').href = url.pathname + url.search;
23956
+ setTabInUrl(url, 'query');
23957
+ document.getElementById('query-tab-button').href = url.pathname + url.search;
23958
+ }
23959
+
23960
+ function selectTab (event, tab) {
23961
+ const url = new URL(window.location);
23962
+ setTabInUrl(url, tab);
23963
+ route(event.target, event, url);
23964
+ }
23965
+
23966
+ function route (target = null, event = null, url = null) {
23967
+ if (url) {
23968
+ if (event) {
23969
+ event.preventDefault();
23970
+ if (!(target instanceof EditorView && event instanceof KeyboardEvent)) {
23971
+ if (event.shiftKey) {
23972
+ window.open(url).focus();
23973
+ return
23974
+ }
23975
+ if (event.metaKey) {
23976
+ window.open(url, '_blank').focus();
23977
+ return
23978
+ }
23920
23979
  }
23921
23980
  }
23922
- } else {
23923
- if (tab !== 'query') {
23924
- url.searchParams.set('tab', tab);
23981
+ if (url.href !== window.location.href) {
23925
23982
  window.history.pushState({}, '', url);
23926
- return route()
23927
23983
  }
23984
+ } else {
23985
+ url = new URL(window.location);
23928
23986
  }
23987
+ updateTabs();
23988
+ window.tab = getTabFromUrl(url);
23929
23989
 
23930
- const tabElement = document.getElementById(`${tab}-tab-button`);
23990
+ const tabElement = document.getElementById(`${window.tab}-tab-button`);
23931
23991
  Array.prototype.forEach.call(document.getElementsByClassName('selected-tab-button'), function (selected) {
23932
23992
  selected.classList.remove('selected-tab-button');
23933
23993
  selected.classList.add('tab-button');
@@ -23939,9 +23999,9 @@ var sqlui = (function (exports) {
23939
23999
  selected.style.display = 'none';
23940
24000
  });
23941
24001
 
23942
- switch (tab) {
24002
+ switch (window.tab) {
23943
24003
  case 'query':
23944
- selectQueryTab();
24004
+ selectResultTab();
23945
24005
  break
23946
24006
  case 'graph':
23947
24007
  selectGraphTab();
@@ -23953,7 +24013,7 @@ var sqlui = (function (exports) {
23953
24013
  selectStructureTab();
23954
24014
  break
23955
24015
  default:
23956
- throw new Error(`Unexpected tab: ${tab}`)
24016
+ throw new Error(`Unexpected tab: ${window.tab}`)
23957
24017
  }
23958
24018
  }
23959
24019
 
@@ -24096,42 +24156,26 @@ var sqlui = (function (exports) {
24096
24156
  }
24097
24157
 
24098
24158
  function selectGraphTab () {
24099
- Array.prototype.forEach.call(document.getElementsByClassName('graph-element'), function (selected) {
24100
- selected.style.display = 'flex';
24101
- });
24102
-
24103
- google.charts.load('current', { packages: ['corechart', 'line'] });
24104
- google.charts.setOnLoadCallback(function () {
24105
- loadQueryOrGraphTab(loadGraphResult, queryErrorCallback('graph-status'));
24106
- });
24159
+ document.getElementById('query-box').style.display = 'flex';
24160
+ document.getElementById('submit-box').style.display = 'flex';
24161
+ document.getElementById('graph-box').style.display = 'flex';
24162
+ document.getElementById('graph-status').style.display = 'flex';
24163
+ maybeFetchResult();
24107
24164
 
24108
24165
  const selection = getSelection();
24109
24166
  focus();
24110
24167
  setSelection(selection);
24111
24168
  }
24112
24169
 
24113
- function selectQueryTab () {
24114
- Array.prototype.forEach.call(document.getElementsByClassName('query-element'), function (selected) {
24115
- selected.style.display = 'flex';
24116
- });
24117
-
24170
+ function selectResultTab () {
24171
+ document.getElementById('query-box').style.display = 'flex';
24172
+ document.getElementById('submit-box').style.display = 'flex';
24173
+ document.getElementById('result-box').style.display = 'flex';
24174
+ document.getElementById('result-status').style.display = 'flex';
24118
24175
  const selection = getSelection();
24119
24176
  focus();
24120
24177
  setSelection(selection);
24121
-
24122
- loadQueryOrGraphTab(loadQueryResult, queryErrorCallback('query-status'));
24123
- }
24124
-
24125
- function queryErrorCallback (statusElementId) {
24126
- const statusElement = document.getElementById(statusElementId);
24127
- return function (message, error) {
24128
- if (error) {
24129
- console.log(`${message}\n${error}`);
24130
- statusElement.innerText = `error: ${message} (check console)`;
24131
- } else {
24132
- statusElement.innerText = `error: ${message}`;
24133
- }
24134
- }
24178
+ maybeFetchResult();
24135
24179
  }
24136
24180
 
24137
24181
  function selectSavedTab () {
@@ -24149,22 +24193,20 @@ var sqlui = (function (exports) {
24149
24193
  }
24150
24194
 
24151
24195
  const saved = window.metadata.saved;
24152
- if (saved && saved.length === 1) {
24196
+ if (Object.keys(saved).length === 1) {
24153
24197
  setSavedStatus('1 file');
24154
24198
  } else {
24155
24199
  setSavedStatus(`${saved.length} files`);
24156
24200
  }
24157
- saved.forEach(file => {
24201
+ Object.values(saved).forEach(file => {
24158
24202
  const divElement = document.createElement('div');
24159
24203
  divElement.classList.add('saved-list-item');
24160
24204
  divElement.addEventListener('click', function (event) {
24161
24205
  clearResult();
24162
- const url = new URL(window.location);
24163
- url.searchParams.delete('sql');
24164
- url.searchParams.delete('tab');
24206
+ const url = new URL(window.location.origin + window.location.pathname);
24207
+ setTabInUrl(url, 'query');
24165
24208
  url.searchParams.set('file', file.filename);
24166
- window.history.pushState({}, '', url);
24167
- route();
24209
+ route(event.target, event, url);
24168
24210
  });
24169
24211
  const nameElement = document.createElement('h2');
24170
24212
  nameElement.innerText = file.filename;
@@ -24179,59 +24221,68 @@ var sqlui = (function (exports) {
24179
24221
  window.savedLoaded = true;
24180
24222
  }
24181
24223
 
24182
- function submitAll () {
24183
- submit(null);
24224
+ function submitAll (target, event) {
24225
+ submit(target, event);
24184
24226
  }
24185
24227
 
24186
- function submitCurrent () {
24187
- submit(getSelection());
24228
+ function submitCurrent (target, event) {
24229
+ submit(target, event, getSelection());
24188
24230
  }
24189
24231
 
24190
- function submit (selection) {
24232
+ function submit (target, event, selection = null) {
24233
+ clearResult();
24191
24234
  const url = new URL(window.location);
24192
- if (selection) {
24193
- url.searchParams.set('selection', selection.join(':'));
24194
- } else {
24195
- url.searchParams.delete('selection');
24196
- }
24197
-
24198
24235
  let sql = getValue().trim();
24199
24236
  sql = sql === '' ? null : sql;
24200
24237
 
24238
+ url.searchParams.delete('run');
24239
+
24201
24240
  if (url.searchParams.has('file')) {
24202
- url.searchParams.delete('file');
24203
- url.searchParams.set('sql', sql);
24204
- window.history.pushState({}, '', url);
24241
+ if (window.metadata.saved[url.searchParams.get('file')].contents !== getValue()) {
24242
+ url.searchParams.delete('file');
24243
+ url.searchParams.set('sql', sql);
24244
+ }
24205
24245
  } else {
24206
24246
  let sqlParam = url.searchParams.get('sql')?.trim();
24207
24247
  sqlParam = sqlParam === '' ? null : sqlParam;
24208
24248
 
24209
- if (sqlParam !== sql) {
24210
- if (sql === null) {
24211
- url.searchParams.delete('sql');
24212
- window.history.pushState({}, '', url);
24213
- } else {
24214
- url.searchParams.set('sql', sql);
24215
- window.history.pushState({}, '', url);
24216
- }
24249
+ if (sqlParam !== sql && sql === null) {
24250
+ url.searchParams.delete('sql');
24251
+ } else if (sqlParam !== sql) {
24252
+ url.searchParams.set('sql', sql);
24253
+ }
24254
+ }
24255
+
24256
+ if (sql) {
24257
+ if (selection) {
24258
+ url.searchParams.set('selection', selection);
24217
24259
  } else {
24218
- window.history.replaceState({}, '', url);
24260
+ url.searchParams.delete('selection');
24219
24261
  }
24262
+ } else {
24263
+ url.searchParams.delete('selection');
24264
+ url.searchParams.delete('sql');
24265
+ url.searchParams.delete('file');
24220
24266
  }
24221
- clearResult();
24222
- route();
24267
+
24268
+ route(target, event, url);
24223
24269
  }
24224
24270
 
24225
24271
  function clearResult () {
24226
24272
  clearGraphStatus();
24227
- clearQueryStatus();
24273
+ clearResultStatus();
24228
24274
  clearGraphBox();
24229
24275
  clearResultBox();
24230
- window.result = null;
24276
+ const existingRequest = window.sqlFetch;
24277
+ if (existingRequest?.state === 'pending') {
24278
+ existingRequest.state = 'aborted';
24279
+ existingRequest.fetchController.abort();
24280
+ }
24281
+ window.sqlFetch = null;
24231
24282
  }
24232
24283
 
24233
- function clearQueryStatus () {
24234
- document.getElementById('query-status').innerText = '';
24284
+ function clearResultStatus () {
24285
+ document.getElementById('result-status').innerText = '';
24235
24286
  }
24236
24287
 
24237
24288
  function clearGraphStatus () {
@@ -24252,7 +24303,7 @@ var sqlui = (function (exports) {
24252
24303
  }
24253
24304
  }
24254
24305
 
24255
- function fetchSql (sql, selection, successCallback, errorCallback) {
24306
+ function fetchSql (request, selection, callback) {
24256
24307
  fetch('query', {
24257
24308
  headers: {
24258
24309
  Accept: 'application/json',
@@ -24260,137 +24311,165 @@ var sqlui = (function (exports) {
24260
24311
  },
24261
24312
  method: 'POST',
24262
24313
  body: JSON.stringify({
24263
- sql,
24314
+ sql: request.sql,
24264
24315
  selection
24265
- })
24316
+ }),
24317
+ signal: request.fetchController.signal
24266
24318
  })
24267
24319
  .then((response) => {
24268
24320
  const contentType = response.headers.get('content-type');
24269
24321
  if (contentType && contentType.indexOf('application/json') !== -1) {
24270
24322
  response.json().then((result) => {
24271
24323
  if (result?.query) {
24272
- successCallback(result);
24273
- } else if (result?.error) {
24274
- errorCallback(result.error, result.stacktrace);
24275
- } else if (result) {
24276
- errorCallback('failed to execute query', result.toString());
24324
+ request.state = 'success';
24325
+ request.result = result;
24277
24326
  } else {
24278
- errorCallback('failed to execute query');
24327
+ request.state = 'error';
24328
+ if (result?.error) {
24329
+ request.error_message = result.error;
24330
+ request.error_details = result.stacktrace;
24331
+ } else if (result) {
24332
+ request.error_message = 'failed to execute query';
24333
+ request.error_details = result.toString();
24334
+ } else {
24335
+ request.error_message = 'failed to execute query';
24336
+ }
24279
24337
  }
24338
+ callback(request);
24280
24339
  });
24281
24340
  } else {
24282
24341
  response.text().then((result) => {
24283
- errorCallback('failed to execute query', result);
24342
+ request.state = 'error';
24343
+ request.error_message = 'failed to execute query';
24344
+ request.error_details = result;
24345
+ callback(request);
24284
24346
  });
24285
24347
  }
24286
24348
  })
24287
24349
  .catch(function (error) {
24288
- errorCallback('failed to execute query', error.stack);
24289
- });
24290
- }
24291
-
24292
- function fetchFile (name, successCallback, errorCallback) {
24293
- fetch(`query_file?file=${name}`, {
24294
- headers: {
24295
- Accept: 'application/json'
24296
- },
24297
- method: 'GET'
24298
- })
24299
- .then((response) => {
24300
- const contentType = response.headers.get('content-type');
24301
- if (contentType && contentType.indexOf('application/json') !== -1) {
24302
- response.json().then((result) => {
24303
- if (result?.query) {
24304
- successCallback(result);
24305
- } else if (result?.error) {
24306
- errorCallback(result.error, result.stacktrace);
24307
- } else if (result) {
24308
- errorCallback('failed to load file ', result.toString());
24309
- } else {
24310
- errorCallback('failed to load file');
24311
- }
24312
- });
24313
- } else {
24314
- errorCallback('failed to load file', response.toString());
24350
+ if (request.state === 'pending') {
24351
+ request.state = 'error';
24352
+ request.error_message = 'failed to execute query';
24353
+ request.error_details = error.stack;
24354
+ callback(request);
24315
24355
  }
24316
- })
24317
- .catch(function (error) {
24318
- errorCallback('failed to load file', error.stack);
24319
24356
  });
24320
24357
  }
24321
24358
 
24322
- function loadQueryOrGraphTab (callback, errorCallback) {
24359
+ function maybeFetchResult () {
24323
24360
  const params = new URLSearchParams(window.location.search);
24324
24361
  const sql = params.get('sql');
24325
24362
  const file = params.get('file');
24326
- const selection = params.has('selection') ? params.get('selection') : null;
24363
+ const selection = params.get('selection');
24364
+ const run = !params.has('run') || !['0', 'false'].includes(params.get('run').toLowerCase());
24327
24365
 
24328
- if (params.has('sql') && window.result && sql === window.result.query) {
24329
- callback();
24330
- return
24331
- } else if (params.has('file') && window.result && file === window.result.file) {
24332
- callback();
24333
- return
24334
- }
24335
-
24336
- if (params.has('file') && params.has('sql') && selection === window.result.selection) {
24366
+ if (params.has('file') && params.has('sql')) {
24337
24367
  // TODO: show an error.
24338
24368
  throw new Error('You can only specify a file or sql, not both.')
24339
24369
  }
24340
24370
 
24371
+ const request = {
24372
+ fetchController: new AbortController(),
24373
+ state: 'pending',
24374
+ sql,
24375
+ file,
24376
+ selection
24377
+ };
24378
+
24379
+ if (params.has('file')) {
24380
+ const fileDetails = window.metadata.saved[params.get('file')];
24381
+ if (!fileDetails) {
24382
+ throw new Error(`no such file: ${params.get('file')}`)
24383
+ }
24384
+ request.file = file;
24385
+ request.sql = fileDetails.contents;
24386
+ } else if (params.has('sql')) {
24387
+ request.sql = sql;
24388
+ }
24389
+
24390
+ const existingRequest = window.sqlFetch;
24391
+ if (existingRequest) {
24392
+ const selectionMatches = selection === existingRequest.selection;
24393
+ const sqlMatches = params.has('sql') && sql === existingRequest.sql;
24394
+ const fileMatches = params.has('file') && file === existingRequest.file;
24395
+ const queryMatches = sqlMatches || fileMatches;
24396
+ if (selectionMatches && queryMatches) {
24397
+ displaySqlFetch(existingRequest);
24398
+ if (params.has('selection')) {
24399
+ focus();
24400
+ setSelection(selection);
24401
+ }
24402
+ return
24403
+ }
24404
+ }
24405
+
24341
24406
  clearResult();
24342
24407
 
24343
- if (params.has('sql')) {
24344
- setValue(sql);
24345
- fetchSql(params.get('sql'), selection, function (result) {
24346
- window.result = result;
24347
- callback();
24348
- }, errorCallback);
24349
- } else if (params.has('file')) {
24350
- setValue('');
24351
- fetchFile(file, function (result) {
24352
- window.result = result;
24353
- setValue(result.query);
24354
- callback();
24355
- }, errorCallback);
24408
+ if (params.has('sql') || params.has('file')) {
24409
+ setValue(request.sql);
24410
+ if (run) {
24411
+ window.sqlFetch = request;
24412
+ displaySqlFetch(request);
24413
+ fetchSql(request, selection, displaySqlFetch);
24414
+ }
24356
24415
  }
24357
24416
  if (params.has('selection')) {
24358
24417
  focus();
24359
- setSelection(params.get('selection').split(':'));
24418
+ setSelection(selection);
24360
24419
  }
24361
24420
  }
24362
24421
 
24363
- function loadQueryResult () {
24364
- const resultElement = document.getElementById('result-box');
24365
- if (resultElement.children.length > 0) {
24422
+ function displaySqlFetchInResultTab (fetch) {
24423
+ const fetchSqlBoxElement = document.getElementById('fetch-sql-box');
24424
+ const resultBoxElement = document.getElementById('result-box');
24425
+ if (fetch.state === 'pending') {
24426
+ clearResultBox();
24427
+ resultBoxElement.style.display = 'none';
24428
+ fetchSqlBoxElement.style.display = 'flex';
24366
24429
  return
24367
24430
  }
24368
24431
 
24369
- setQueryStatus(window.result);
24432
+ resultBoxElement.style.display = 'flex';
24433
+ fetchSqlBoxElement.style.display = 'none';
24434
+
24435
+ if (fetch.state === 'error') {
24436
+ clearResultBox();
24437
+ displaySqlFetchError('result-status', fetch.error_message, fetch.error_details);
24438
+ return
24439
+ }
24370
24440
 
24371
- // if (!window.result.rows) {
24372
- // return
24373
- // }
24441
+ if (fetch.state !== 'success') {
24442
+ throw new Error(`unexpected fetch sql request status: ${fetch.status}`)
24443
+ }
24444
+
24445
+ if (document.getElementById('result-table')) {
24446
+ // Results already displayed.
24447
+ return
24448
+ }
24449
+
24450
+ clearResultBox();
24451
+ displaySqlFetchResultStatus('result-status', fetch.result);
24374
24452
 
24375
24453
  const tableElement = document.createElement('table');
24454
+ tableElement.id = 'result-table';
24376
24455
  const theadElement = document.createElement('thead');
24377
24456
  const headerElement = document.createElement('tr');
24378
24457
  const tbodyElement = document.createElement('tbody');
24379
24458
  theadElement.appendChild(headerElement);
24380
24459
  tableElement.appendChild(theadElement);
24381
24460
  tableElement.appendChild(tbodyElement);
24382
- resultElement.appendChild(tableElement);
24461
+ resultBoxElement.appendChild(tableElement);
24383
24462
 
24384
- window.result.columns.forEach(column => {
24463
+ fetch.result.columns.forEach(column => {
24385
24464
  const template = document.createElement('template');
24386
24465
  template.innerHTML = `<th class="cell">${column}</th>`;
24387
24466
  headerElement.appendChild(template.content.firstChild);
24388
24467
  });
24389
- if (window.result.columns.length > 0) {
24468
+ if (fetch.result.columns.length > 0) {
24390
24469
  headerElement.appendChild(document.createElement('th'));
24391
24470
  }
24392
24471
  let highlight = false;
24393
- window.result.rows.forEach(function (row) {
24472
+ fetch.result.rows.forEach(function (row) {
24394
24473
  const rowElement = document.createElement('tr');
24395
24474
  if (highlight) {
24396
24475
  rowElement.classList.add('highlighted-row');
@@ -24404,29 +24483,67 @@ var sqlui = (function (exports) {
24404
24483
  });
24405
24484
  rowElement.appendChild(document.createElement('td'));
24406
24485
  });
24486
+ }
24407
24487
 
24408
- document.getElementById('result-box').style.display = 'flex';
24488
+ function displaySqlFetch (fetch) {
24489
+ if (window.tab === 'query') {
24490
+ displaySqlFetchInResultTab(fetch);
24491
+ } else if (window.tab === 'graph') {
24492
+ displaySqlFetchInGraphTab(fetch);
24493
+ }
24494
+ }
24495
+
24496
+ function displaySqlFetchError (statusElementId, message, details) {
24497
+ const statusElement = document.getElementById(statusElementId);
24498
+ if (details) {
24499
+ console.log(`${message}\n${details}`);
24500
+ statusElement.innerText = `error: ${message} (check console)`;
24501
+ } else {
24502
+ statusElement.innerText = `error: ${message}`;
24503
+ }
24409
24504
  }
24410
24505
 
24411
- function loadGraphResult () {
24412
- setGraphStatus(window.result);
24506
+ function displaySqlFetchInGraphTab (fetch) {
24507
+ const graphBoxElement = document.getElementById('graph-box');
24508
+ const fetchSqlBoxElement = document.getElementById('fetch-sql-box');
24509
+ if (fetch.state === 'pending') {
24510
+ clearGraphBox();
24511
+ graphBoxElement.style.display = 'none';
24512
+ fetchSqlBoxElement.style.display = 'flex';
24513
+ return
24514
+ }
24515
+
24516
+ graphBoxElement.style.display = 'flex';
24517
+ fetchSqlBoxElement.style.display = 'none';
24413
24518
 
24414
- if (!window.result.rows) {
24519
+ if (fetch.state === 'error') {
24520
+ clearGraphBox();
24521
+ displaySqlFetchError('graph-status', fetch.error_message, fetch.error_details);
24415
24522
  return
24416
24523
  }
24417
- if (window.result.rows.length === 0 || window.result.columns.length < 2) {
24524
+
24525
+ if (fetch.state !== 'success') {
24526
+ throw new Error(`unexpected fetch sql request status: ${fetch.status}`)
24527
+ }
24528
+ clearGraphBox();
24529
+ displaySqlFetchResultStatus('graph-status', fetch.result);
24530
+
24531
+ if (!fetch.result.rows) {
24532
+ return
24533
+ }
24534
+ if (fetch.result.rows.length === 0 || fetch.result.columns.length < 2) {
24418
24535
  return
24419
24536
  }
24420
24537
  const dataTable = new google.visualization.DataTable();
24421
- window.result.columns.forEach((column, index) => {
24422
- dataTable.addColumn(window.result.column_types[index], column);
24538
+ fetch.result.columns.forEach((column, index) => {
24539
+ dataTable.addColumn(fetch.result.column_types[index], column);
24423
24540
  });
24424
24541
 
24425
- window.result.rows.forEach((row) => {
24542
+ fetch.result.rows.forEach((row) => {
24426
24543
  const rowValues = row.map((value, index) => {
24427
- if (window.result.column_types[index] === 'date' || window.result.column_types[index] === 'datetime') {
24544
+ if (fetch.result.column_types[index] === 'date' || fetch.result.column_types[index] === 'datetime') {
24428
24545
  return new Date(value)
24429
- } else if (window.result.column_types[index] === 'timeofday') {
24546
+ } else if (fetch.result.column_types[index] === 'timeofday') {
24430
24547
  // TODO: This should be hour, minute, second, milliseconds
24431
24548
  return [0, 0, 0, 0]
24432
24549
  } else {
@@ -24436,36 +24553,20 @@ var sqlui = (function (exports) {
24436
24553
  dataTable.addRow(rowValues);
24437
24554
  });
24438
24555
 
24439
- const graphBoxElement = document.getElementById('graph-box');
24440
-
24441
24556
  const chart = new google.visualization.LineChart(graphBoxElement);
24442
24557
  const options = {
24443
24558
  hAxis: {
24444
- title: window.result.columns[0]
24559
+ title: fetch.result.columns[0]
24445
24560
  },
24446
24561
  vAxis: {
24447
- title: window.result.columns[1]
24562
+ title: fetch.result.columns[1]
24448
24563
  }
24449
24564
  };
24450
24565
  chart.draw(dataTable, options);
24451
24566
  }
24452
24567
 
24453
- function setGraphStatus (result) {
24454
- const statusElement = document.getElementById('graph-status');
24455
-
24456
- if (result.total_rows === 1) {
24457
- statusElement.innerText = `${result.total_rows} row`;
24458
- } else {
24459
- statusElement.innerText = `${result.total_rows} rows`;
24460
- }
24461
-
24462
- if (result.total_rows > result.rows.length) {
24463
- statusElement.innerText += ` (truncated to ${result.rows.length})`;
24464
- }
24465
- }
24466
-
24467
- function setQueryStatus (result) {
24468
- const statusElement = document.getElementById('query-status');
24568
+ function displaySqlFetchResultStatus (statusElementId, result) {
24569
+ const statusElement = document.getElementById(statusElementId);
24469
24570
 
24470
24571
  if (result.total_rows === 1) {
24471
24572
  statusElement.innerText = `${result.total_rows} row`;
@@ -24487,24 +24588,24 @@ var sqlui = (function (exports) {
24487
24588
  });
24488
24589
 
24489
24590
  window.addEventListener('resize', function (event) {
24490
- if (window.tab === 'graph' && window.result) {
24591
+ if (window.tab === 'graph' && window.sqlFetch.result) {
24491
24592
  clearGraphBox();
24492
- loadGraphResult();
24593
+ displaySqlFetchInGraphTab(window.sqlFetch);
24493
24594
  }
24494
24595
  });
24495
24596
 
24496
- function route () {
24497
- selectTab(new URLSearchParams(window.location.search).get('tab') || 'query');
24498
- }
24499
-
24500
24597
  window.onload = function () {
24501
- fetch('metadata', {
24502
- headers: {
24503
- Accept: 'application/json'
24504
- },
24505
- method: 'GET'
24506
- })
24507
- .then((response) => {
24598
+ Promise.all([
24599
+ google.charts.load('current', { packages: ['corechart', 'line'] }),
24600
+ fetch('metadata', {
24601
+ headers: {
24602
+ Accept: 'application/json'
24603
+ },
24604
+ method: 'GET'
24605
+ })
24606
+ ])
24607
+ .then((results) => {
24608
+ const response = results[1];
24508
24609
  const contentType = response.headers.get('content-type');
24509
24610
  if (contentType && contentType.indexOf('application/json') !== -1) {
24510
24611
  return response.json().then((result) => {
@@ -24525,7 +24626,9 @@ var sqlui = (function (exports) {
24525
24626
  window.metadata = result;
24526
24627
  document.getElementById('loading-box').style.display = 'none';
24527
24628
  document.getElementById('main-box').style.display = 'flex';
24528
- document.getElementById('server-name').innerText = result.server;
24629
+ document.getElementById('server-name').innerText = window.metadata.server;
24630
+ document.title = `SQLUI ${window.metadata.server}`;
24631
+ document.getElementById('header-link').href = result.list_url_path;
24529
24632
  const queryElement = document.getElementById('query');
24530
24633
 
24531
24634
  init(queryElement, submitCurrent, submitAll);
@@ -24547,10 +24650,4 @@ var sqlui = (function (exports) {
24547
24650
  });
24548
24651
  };
24549
24652
 
24550
- exports.selectTab = selectTab;
24551
- exports.submitAll = submitAll;
24552
- exports.submitCurrent = submitCurrent;
24553
-
24554
- return exports;
24555
-
24556
- })({});
24653
+ })();
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.24
4
+ version: 0.1.25
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-10-28 00:00:00.000000000 Z
11
+ date: 2022-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mysql2