sqlui 0.1.24 → 0.1.25

Sign up to get free protection for your applications and to get access to all the features.
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