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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 580bda7206a3282d1dd0dae3754daa8bdf53b3522c5210a0e2542147d3a13aee
4
- data.tar.gz: 6fcfca4ed80216ef293f72e8be9e59896dbd82eb3da76f5761ad3671fa16c32b
3
+ metadata.gz: a936a2e2476462ad9d92ed315bc02bee65b7c84171d5934e9728241f7a28ba1c
4
+ data.tar.gz: 2cf060fb96f672ebaf6b7fd9be602652e1a30841ded45c28415d7cc3dabc03e1
5
5
  SHA512:
6
- metadata.gz: 6ffaff0a56fd85efbf3c3fe69fad3c5f4750b23defc7716004420fb4fa9243983d6a105dbbe81892c3cf859332530920ab88f4312a9ffb7009e6d46103e3dd9d
7
- data.tar.gz: 94cea75b430cfe9289f28fc9eaaa9a5cdd026945d0fb2e66616cfcc0e6bcd3845fcfcc205ad9b047274e81f50c71313f004fe35ba0fc167216c06a9211c04317
6
+ metadata.gz: addfac3cd62b75320dfcd2a437dac79db358b69391782f008e49be816241c1dad40d641accd078735c46de1d855eaf162956cc2a16370c297a72175e8b0bb7a2
7
+ data.tar.gz: 2a215084e23ac9b7c11d44f325fc27c77a597d3c9ed6f5e1ded238aa7223d82936f6d475674cb777406d491f4699f2a782cafabaf7c36d24be83d72e3eb998e8
data/.version CHANGED
@@ -1 +1 @@
1
- 0.1.28
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
- raise ArgumentError, "required parameter #{key} not a #{classes[0].to_s.downcase}" if classes.size == 1
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
- raise ArgumentError, "required parameter #{key} not #{classes.map(&:to_s).map(&:downcase).join(' or ')}"
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!(&:to_s)
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, Environment.server_port
19
- set :env, Environment.server_env
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': 'text/css'
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': 'text/javascript'
50
+ headers 'Content-Type' => 'text/javascript'
52
51
  body @js
53
52
  end
54
53
 
55
- get "#{database.url_path}/metadata" do
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': 'application/json'
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': 'application/json'
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': 'text/html'
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': 'application/json'
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('Content-Type': 'application/json')
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
- deep_symbolize!(config)
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
@@ -138,7 +138,7 @@ p {
138
138
  font-family: Helvetica, sans-serif;
139
139
  white-space: nowrap;
140
140
  overflow: hidden;
141
- font-size: 18px;
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
+ }
@@ -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;">
@@ -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
- const existingFetch = window.sqlFetch;
24304
- if (existingFetch?.state === 'pending') {
24305
- existingFetch.state = 'aborted';
24306
- existingFetch.fetchController.abort();
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 fetchSql (sqlFetch, selection, callback) {
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
- callback(sqlFetch);
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
- callback(sqlFetch);
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
- callback(sqlFetch);
24413
+ displaySqlFetch(sqlFetch);
24389
24414
  });
24390
24415
  }
24391
24416
 
24392
- function maybeFetchResult () {
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 run = ['1', 'true'].includes(params.get('run')?.toLowerCase());
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
- url.searchParams.delete('run');
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
- document.getElementById('cancel-button').style.display = 'flex';
24462
- document.getElementById('result-box').style.display = 'none';
24463
- document.getElementById('fetch-sql-box').style.display = 'flex';
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.result);
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
- document.getElementById('cancel-button').style.display = 'flex';
24551
- document.getElementById('graph-box').style.display = 'none';
24552
- document.getElementById('fetch-sql-box').style.display = 'flex';
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.result);
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, result) {
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: 'GET'
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.28
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-01 00:00:00.000000000 Z
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: puma
42
+ name: webrick
57
43
  requirement: !ruby/object:Gem::Requirement
58
44
  requirements:
59
45
  - - "~>"
60
46
  - !ruby/object:Gem::Version
61
- version: '6.0'
62
- type: :development
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: '6.0'
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