sqlui 0.1.28 → 0.1.30

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: 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