sqlui 0.1.51 → 0.1.52

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: 1726ad80d8643d99cff2dd4dcf330d35cd109b8277bc6aaa09ae265a66b6018f
4
- data.tar.gz: 9b61be584f56fb65d3239566d79bab94c6a45352201555c853ab726e682c0e86
3
+ metadata.gz: 67496893776f87c67e14c32003e03f5ad2efa2dede7afc7efb6b7d69a6489fcc
4
+ data.tar.gz: c7b0678eada7efa6b22666950b16157e5dfd06bebee46b978e2a8c2339a456e0
5
5
  SHA512:
6
- metadata.gz: 57ea926197ec2949eea9af1ed9a55f0ddee7f3985fbfd8515d0401bbe4404a060f6514cbe71ac0f808aec1fc4d4670f3bd80029eda03b4f7a4bf88806f443225
7
- data.tar.gz: 850c957b833956feb133189cfb1ff0dbffbfa8d31e1ef7065038f26b7cd4a3f3035cc47dd122c344746a130b4bf4e6f6f40a4e24ff7a88a96d8a4c3d4f006475
6
+ metadata.gz: 23df6f8e5acdff2a680e933db3730f989d8e4f6cc51829f091c7aa12651a27ab3179eac4a31eae3d6f4dbd314000628dc0deca94c553f359f599e2a44d25bfbe
7
+ data.tar.gz: 3389a5572cddc8ebe39b7d12eb7fef3d1548aae2a2957b0eeea96fa18e622acc204eb85bc1aec68556757058ac3142b5471e1001a61678a124a7fd55c36a2154
data/.version CHANGED
@@ -1 +1 @@
1
- 0.1.51
1
+ 0.1.52
data/app/server.rb CHANGED
@@ -4,8 +4,11 @@ require 'base64'
4
4
  require 'csv'
5
5
  require 'erb'
6
6
  require 'json'
7
+ require 'prometheus/middleware/collector'
8
+ require 'prometheus/middleware/exporter'
7
9
  require 'sinatra/base'
8
10
  require 'uri'
11
+ require 'webrick'
9
12
  require_relative 'database_metadata'
10
13
  require_relative 'mysql_types'
11
14
  require_relative 'sql_parser'
@@ -18,6 +21,29 @@ class Server < Sinatra::Base
18
21
  end
19
22
 
20
23
  def self.init_and_run(config, resources_dir)
24
+ logger.info("Airbrake enabled: #{config.airbrake[:server]&.[](:enabled) || false}")
25
+ if config.airbrake[:server]&.[](:enabled)
26
+ require 'airbrake'
27
+ require 'airbrake/rack'
28
+
29
+ Airbrake.configure do |c|
30
+ c.app_version = File.read('.version').strip
31
+ c.environment = config.environment
32
+ c.logger.level = Logger::DEBUG if config.environment != :production?
33
+ config.airbrake[:server].each do |key, value|
34
+ c.send("#{key}=".to_sym, value) unless key == :enabled
35
+ end
36
+ end
37
+ Airbrake.add_filter(Airbrake::Rack::RequestBodyFilter.new)
38
+ Airbrake.add_filter(Airbrake::Rack::HttpParamsFilter.new)
39
+ Airbrake.add_filter(Airbrake::Rack::HttpHeadersFilter.new)
40
+ use Airbrake::Rack::Middleware
41
+ end
42
+
43
+ use Rack::Deflater
44
+ use Prometheus::Middleware::Collector
45
+ use Prometheus::Middleware::Exporter
46
+
21
47
  Mysql2::Client.default_query_options[:as] = :array
22
48
  Mysql2::Client.default_query_options[:cast_booleans] = true
23
49
  Mysql2::Client.default_query_options[:database_timezone] = :utc
@@ -53,17 +79,13 @@ class Server < Sinatra::Base
53
79
  end
54
80
 
55
81
  get "#{database.url_path}/sqlui.css" do
56
- @css ||= File.read(File.join(resources_dir, 'sqlui.css'))
57
- status 200
58
82
  headers 'Content-Type' => 'text/css; charset=utf-8'
59
- body @css
83
+ send_file File.join(resources_dir, 'sqlui.css')
60
84
  end
61
85
 
62
86
  get "#{database.url_path}/sqlui.js" do
63
- @js ||= File.read(File.join(resources_dir, 'sqlui.js'))
64
- status 200
65
87
  headers 'Content-Type' => 'text/javascript; charset=utf-8'
66
- body @js
88
+ send_file File.join(resources_dir, 'sqlui.js')
67
89
  end
68
90
 
69
91
  post "#{database.url_path}/metadata" do
@@ -99,7 +121,9 @@ class Server < Sinatra::Base
99
121
  end
100
122
 
101
123
  post "#{database.url_path}/query" do
102
- params.merge!(JSON.parse(request.body.read, symbolize_names: true))
124
+ data = request.body.read
125
+ request.body.rewind # since Airbrake will read the body on error
126
+ params.merge!(JSON.parse(data, symbolize_names: true))
103
127
  break client_error('missing sql') unless params[:sql]
104
128
 
105
129
  variables = params[:variables] || {}
@@ -111,29 +135,42 @@ class Server < Sinatra::Base
111
135
  database.with_client do |client|
112
136
  query_result = execute_query(client, variables, sql)
113
137
  stream do |out|
114
- json = <<~RES.chomp
115
- {
116
- "columns": #{query_result.fields.to_json},
117
- "column_types": #{MysqlTypes.map_to_google_charts_types(query_result.field_types).to_json},
118
- "total_rows": #{query_result.size.to_json},
119
- "selection": #{params[:selection].to_json},
120
- "query": #{params[:sql].to_json},
121
- "rows": [
122
- RES
123
- out << json
124
- bytes = json.bytesize
125
- query_result.each_with_index do |row, i|
126
- json = "#{i.zero? ? '' : ','}\n #{row.to_json}"
127
- bytes += json.bytesize
128
- break if i == Sqlui::MAX_ROWS || bytes > Sqlui::MAX_BYTES
129
-
138
+ if query_result
139
+ json = <<~RES.chomp
140
+ {
141
+ "columns": #{query_result.fields.to_json},
142
+ "column_types": #{MysqlTypes.map_to_google_charts_types(query_result.field_types).to_json},
143
+ "total_rows": #{query_result.size.to_json},
144
+ "selection": #{params[:selection].to_json},
145
+ "query": #{params[:sql].to_json},
146
+ "rows": [
147
+ RES
130
148
  out << json
131
- end
132
- out << <<~RES
149
+ bytes = json.bytesize
150
+ query_result.each_with_index do |row, i|
151
+ json = "#{i.zero? ? '' : ','}\n #{row.to_json}"
152
+ bytes += json.bytesize
153
+ break if i == Sqlui::MAX_ROWS || bytes > Sqlui::MAX_BYTES
154
+
155
+ out << json
156
+ end
157
+ out << <<~RES
133
158
 
134
- ]
135
- }
136
- RES
159
+ ]
160
+ }
161
+ RES
162
+ else
163
+ out << <<~RES
164
+ {
165
+ "columns": [],
166
+ "column_types": [],
167
+ "total_rows": 0,
168
+ "selection": #{params[:selection].to_json},
169
+ "query": #{params[:sql].to_json},
170
+ "rows": []
171
+ }
172
+ RES
173
+ end
137
174
  end
138
175
  end
139
176
  end
@@ -161,10 +198,14 @@ class Server < Sinatra::Base
161
198
  end
162
199
 
163
200
  get(%r{#{Regexp.escape(database.url_path)}/(query|graph|structure|saved)}) do
164
- @html ||= File.read(File.join(resources_dir, 'sqlui.html'))
165
201
  status 200
166
- headers 'Content-Type' => 'text/html; charset=utf-8'
167
- body @html
202
+ client_config = config.airbrake[:client] || {}
203
+ erb :sqlui, locals: {
204
+ environment: config.environment.to_s,
205
+ airbrake_enabled: client_config[:enabled] || false,
206
+ airbrake_project_id: client_config[:project_id] || '',
207
+ airbrake_project_key: client_config[:project_key] || ''
208
+ }
168
209
  end
169
210
  end
170
211
 
data/app/sqlui_config.rb CHANGED
@@ -8,7 +8,7 @@ require_relative 'deep'
8
8
 
9
9
  # App config including database configs.
10
10
  class SqluiConfig
11
- attr_reader :name, :port, :environment, :list_url_path, :database_configs
11
+ attr_reader :name, :port, :environment, :list_url_path, :database_configs, :airbrake
12
12
 
13
13
  def initialize(filename, overrides = {})
14
14
  config = YAML.safe_load(ERB.new(File.read(filename)).result, aliases: true).deep_merge!(overrides)
@@ -29,6 +29,7 @@ class SqluiConfig
29
29
  @database_configs = databases.map do |_, current|
30
30
  DatabaseConfig.new(current)
31
31
  end
32
+ @airbrake = Args.fetch_optional_hash(config, :airbrake) || { enabled: false }
32
33
  end
33
34
 
34
35
  def database_config_for(url_path:)
@@ -1,3 +1,4 @@
1
+ <!DOCTYPE html>
1
2
  <html lang="en">
2
3
  <head>
3
4
  <meta charset="utf-8">
data/app/views/error.erb CHANGED
@@ -1,3 +1,4 @@
1
+ <!DOCTYPE html>
1
2
  <html>
2
3
  <html lang="en">
3
4
  <head>
@@ -3,7 +3,22 @@
3
3
  <meta charset="utf-8">
4
4
  <title>SQLUI</title>
5
5
  <link rel="icon" type="image/x-icon" href="/favicon.svg">
6
- <script src="sqlui.js"></script>
6
+ <!-- Initialize Airbrake before loading the main app JS so that we can catch errors as early as possible. -->
7
+ <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@airbrake/browser"></script>
8
+ <script type="text/javascript">
9
+ <% if airbrake_enabled %>
10
+ window.airbrake = new Airbrake.Notifier({
11
+ environment: "<%= environment %>",
12
+ projectId: "<%= airbrake_project_id %>",
13
+ projectKey: "<%= airbrake_project_key %>"
14
+ })
15
+ <% end %>
16
+
17
+ window.notifyAirbrake = function (error) {
18
+ window.airbrake?.notify(error)
19
+ }
20
+ </script>
21
+ <script type="text/javascript" src="sqlui.js"></script>
7
22
  <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
8
23
  <link rel="stylesheet" href="sqlui.css">
9
24
  </head>
@@ -245,6 +245,9 @@ p {
245
245
  display: flex;
246
246
  flex-direction: column;
247
247
  }
248
+ #result-table tbody tr td{
249
+ height: calc(21px + 10px); // 21 for text, 10 for top and bottom padding of 5
250
+ }
248
251
 
249
252
  #result-table tbody tr td abbr a {
250
253
  color: #555;
@@ -23922,6 +23922,7 @@
23922
23922
  const schemas = Object.entries(metadata.schemas);
23923
23923
  const editorSchema = {};
23924
23924
  const tables = [];
23925
+ const aliases = [];
23925
23926
  schemas.forEach(([schemaName, schema]) => {
23926
23927
  Object.entries(schema.tables).forEach(([tableName, table]) => {
23927
23928
  const qualifiedTableName = schemas.length === 1 ? tableName : `${schemaName}.${tableName}`;
@@ -23929,30 +23930,48 @@
23929
23930
  const columns = Object.keys(table.columns);
23930
23931
  editorSchema[qualifiedTableName] = columns;
23931
23932
  const alias = metadata.tables[qualifiedTableName]?.alias;
23932
- const boost = metadata.tables[qualifiedTableName]?.boost;
23933
+ if (alias) {
23934
+ aliases.push(alias);
23935
+ aliases.push(`\`${alias}\``);
23936
+ }
23937
+
23938
+ let boost = metadata.tables[qualifiedTableName]?.boost;
23939
+ boost = boost ? boost * 2 : null;
23933
23940
  if (alias) {
23934
23941
  editorSchema[alias] = columns;
23942
+ // Add a completion which inserts the table name and alias for use just after join or from.
23935
23943
  tables.push({
23936
- label: qualifiedTableName,
23937
- detail: alias,
23938
- boost,
23939
- alias_type: 'with',
23944
+ label: `${qualifiedTableName} ${alias}`,
23945
+ boost: boost + 1,
23946
+ completion_types: ['table_with_alias'],
23940
23947
  quoted: `${quotedQualifiedTableName} \`${alias}\``,
23941
23948
  unquoted: `${qualifiedTableName} ${alias}`
23942
23949
  });
23950
+ // Add a completion which only inserts the alias for use with "select" or "where" or "on".
23943
23951
  tables.push({
23944
- label: qualifiedTableName,
23945
- detail: alias,
23946
- boost,
23947
- alias_type: 'only',
23952
+ label: `${qualifiedTableName} ${alias}`,
23953
+ boost: boost + 1,
23954
+ type: 'constant',
23955
+ completion_types: ['alias_only'],
23948
23956
  quoted: '`' + alias + '`',
23949
23957
  unquoted: alias
23950
23958
  });
23951
- } else {
23952
23959
  tables.push({
23953
- label: qualifiedTableName
23960
+ label: alias,
23961
+ boost: boost + 1,
23962
+ type: 'constant',
23963
+ completion_types: ['alias_only'],
23964
+ quoted: '`' + alias + '`',
23965
+ unquoted: alias
23954
23966
  });
23955
23967
  }
23968
+ tables.push({
23969
+ label: qualifiedTableName,
23970
+ boost,
23971
+ completion_types: ['table_with_alias', 'alias_only', 'table_only'],
23972
+ quoted: quotedQualifiedTableName,
23973
+ unquoted: qualifiedTableName
23974
+ });
23956
23975
  });
23957
23976
  });
23958
23977
  // I prefer to use Cmd-Enter/Ctrl-Enter to submit the query. Here I am replacing the default mapping.
@@ -23992,66 +24011,52 @@
23992
24011
  tables
23993
24012
  };
23994
24013
  const originalSchemaCompletionSource = schemaCompletionSource(sqlConfig);
23995
- const originalKeywordCompletionSource = keywordCompletionSource(MySQL, true);
23996
- const keywordCompletions = [];
24014
+
24015
+ const joinCompletions = [];
23997
24016
  metadata.joins.forEach((join) => {
23998
- ['JOIN', 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'CROSS JOIN'].forEach((type) => {
23999
- keywordCompletions.push({ label: `${type} ${join.label}`, apply: `${type} ${join.apply}`, type: 'keyword' });
24000
- });
24017
+ joinCompletions.push({ label: join.label, apply: join.apply, type: 'keyword' });
24001
24018
  });
24002
- let combinedKeywordCompletionSource;
24003
- if (keywordCompletions.length > 0) {
24004
- const customKeywordCompletionSource = ifNotIn(['QuotedIdentifier', 'SpecialVar', 'String', 'LineComment', 'BlockComment', '.'], completeFromList(keywordCompletions));
24005
- combinedKeywordCompletionSource = function (context) {
24006
- const original = originalKeywordCompletionSource(context);
24007
- const custom = customKeywordCompletionSource(context);
24008
- if (original?.options && custom?.options) {
24009
- original.options = original.options.concat(custom.options);
24010
- }
24011
- return original
24012
- };
24013
- } else {
24014
- combinedKeywordCompletionSource = originalKeywordCompletionSource;
24015
- }
24019
+ const customJoinCompletionSource = completeFromList(joinCompletions);
24020
+
24016
24021
  const sqlExtension = new LanguageSupport(
24017
24022
  MySQL.language,
24018
24023
  [
24019
24024
  MySQL.language.data.of({
24020
24025
  autocomplete: (context) => {
24021
24026
  const result = originalSchemaCompletionSource(context);
24022
- if (!result?.options) return result
24027
+ if (!result?.options || result.options.length === 0) return result
24023
24028
 
24024
24029
  const tree = syntaxTree(context.state);
24025
24030
  let node = tree.resolveInner(context.pos, -1);
24026
24031
  if (!node) return result
24027
24032
 
24028
- // We are trying to identify the case where we are autocompleting a table name after "from" or "join"
24029
-
24030
- // TODO: we don't handle the case where a user typed "select table.foo from". In that case we probably
24031
- // shouldn't autocomplete the alias. Though, if the user typed "select table.foo, t.bar", we won't know
24032
- // what to do. Maybe it is ok to force users to simply delete the alias after autocompleting.
24033
-
24034
- // TODO: if table aliases aren't enabled, we don't need to override autocomplete.
24035
-
24033
+ // We want to customize the autocomplete options based on context. For instance, after select or where, we
24034
+ // should autocomplete aliases, if they are defined, otherwise table names. After from, we should autocomplete
24035
+ // table names with aliases. Etc. We start by trying to identify the node before our current position. The
24036
+ // method for accomplishing this seems to vary based on the user's context.
24036
24037
  let foundSchema;
24037
24038
  if (node.name === 'Statement') {
24038
- // The node can be a Statement if the cursor is at the end of "from " and there is a complete
24039
- // statement in the editor (semicolon present). In that case we want to find the node just before the
24040
- // current position so that we can check whether it is "from" or "join".
24039
+ // The current node can be a Statement if the cursor is at the end of "from " (for instance) and there is a
24040
+ // complete statement in the editor (semicolon present). In that case we want to find the node just before
24041
+ // the current position.
24041
24042
  node = node.childBefore(context.pos);
24042
24043
  } else if (node.name === 'Script') {
24043
24044
  // It seems the node can sometimes be a Script if the cursor is at the end of the last statement in the
24044
24045
  // editor and the statement doesn't end in a semicolon. In that case we can find the last statement in the
24045
- // Script so that we can check whether it is "from" or "join".
24046
+ // Script.
24046
24047
  node = node.lastChild?.childBefore(context.pos);
24048
+ } else if (node.name === 'Parens') {
24049
+ // The current node can be a Parens if we are inside of a function or sub query, for instance just after a
24050
+ // space after a "select" in "select * from (select ) as foo". In that case we can find the last statement
24051
+ // in the Parens.
24052
+ node = node.childBefore(context.pos);
24047
24053
  } else if (['Identifier', 'QuotedIdentifier', 'Keyword', '.'].includes(node.name)) {
24048
24054
  // If the node is an Identifier, we might be in the middle of typing the table name. If the node is a
24049
- // Keyword but isn't "from" or "join", we might be in the middle of typing a table name that is similar
24050
- // to a Keyword, for instance "orders" or "selections" or "fromages". In these cases, look for the previous
24051
- // sibling so that we can check whether it is "from" or "join". If we found a '.' or if the previous
24052
- // sibling is a '.', we might be in the middle of typing something like "schema.table" or
24053
- // "`schema`.table" or "`schema`.`table`". In these cases we need to record the schema used so that we
24054
- // can autocomplete table names with aliases.
24055
+ // Keyword, we might be in the middle of typing a table name that is similar to a Keyword, for instance
24056
+ // "orders" or "selections" or "fromages". In these cases, look for the previous sibling. If the node is a
24057
+ // '.' or if the previous sibling is a '.', we might be in the middle of typing something like
24058
+ // "schema.table" or "`schema`.table" or "`schema`.`table`". In these cases we need to record the schema
24059
+ // used so that we can autocomplete table names with aliases.
24055
24060
  if (node.name !== '.') {
24056
24061
  node = node.prevSibling;
24057
24062
  }
@@ -24068,29 +24073,54 @@
24068
24073
  }
24069
24074
  }
24070
24075
 
24076
+ // We now have the node before the node the user is currently creating. Step back or up until we find a Keyword.
24077
+ while (true) {
24078
+ const nodeText = node ? context.state.doc.sliceString(node.from, node.to).toLowerCase() : null;
24079
+ if (!node || (node.name === 'Keyword' &&
24080
+ ['where', 'select', 'from', 'into', 'join', 'straight_join', 'database', 'as'].includes(nodeText))) {
24081
+ break
24082
+ }
24083
+
24084
+ node = node?.prevSibling || node?.parent;
24085
+ }
24071
24086
  const nodeText = node ? context.state.doc.sliceString(node.from, node.to).toLowerCase() : null;
24072
- if (node?.name === 'Keyword' && ['from', 'join'].includes(nodeText)) {
24073
- result.options = result.options.filter((option) => {
24074
- return option.alias_type === undefined || option.alias_type === 'with'
24075
- });
24076
- } else {
24077
- result.options = result.options.filter((option) => {
24078
- return option.alias_type === undefined || option.alias_type === 'only'
24079
- });
24087
+ let completionType = 'table_only';
24088
+ if (node?.name === 'Keyword') {
24089
+ if (['from', 'join', 'straight_join'].includes(nodeText)) {
24090
+ completionType = 'table_with_alias';
24091
+ } else if (['where', 'select'].includes(nodeText)) {
24092
+ completionType = 'alias_only';
24093
+ }
24094
+
24095
+ if (['join', 'straight_join'].includes(nodeText)) {
24096
+ const customJoins = customJoinCompletionSource(context);
24097
+ if (customJoins?.options) {
24098
+ result.options = result.options.concat(customJoins.options);
24099
+ }
24100
+ }
24080
24101
  }
24102
+ result.options = result.options.filter((option) => {
24103
+ if (option.completion_types === undefined && option.type === 'constant' && aliases.includes(option.label)) {
24104
+ // The default options already include an alias if the statement includes one in the from clause.
24105
+ // In that case we want to remove it in favor of our own alias option.
24106
+ return false
24107
+ }
24108
+ // Allow any options we didn't create plus options we created which are of the expected type.
24109
+ return option.completion_types === undefined || option.completion_types.includes(completionType)
24110
+ });
24081
24111
  result.options = result.options.map((option) => {
24082
24112
  // Some shenanigans. If the default autocomplete function quoted the label, we want to ensure the quote
24083
24113
  // only applies to the table name and not the alias. You might think we could do this by overriding the
24084
24114
  // apply function but apply is set to null when quoting.
24085
24115
  // See https://github.com/codemirror/lang-sql/blob/ebf115fffdbe07f91465ccbd82868c587f8182bc/src/complete.ts#L90
24086
- if (option.alias_type) {
24116
+ if (option.quoted) {
24087
24117
  if (option.label.match(/^`.*`$/)) {
24088
24118
  option.apply = option.quoted;
24089
24119
  } else {
24090
24120
  option.apply = option.unquoted;
24091
24121
  }
24092
24122
  }
24093
- if (foundSchema) {
24123
+ if (foundSchema && completionType === 'table_with_alias') {
24094
24124
  const unquotedLabel = unquoteSqlId(option.label);
24095
24125
  const quoted = unquotedLabel !== option.label;
24096
24126
  const tableConfig = metadata.tables[`${foundSchema}.${unquotedLabel}`];
@@ -24113,7 +24143,7 @@
24113
24143
  }
24114
24144
  }),
24115
24145
  MySQL.language.data.of({
24116
- autocomplete: combinedKeywordCompletionSource
24146
+ autocomplete: keywordCompletionSource(MySQL, true)
24117
24147
  })
24118
24148
  ]
24119
24149
  );
@@ -24952,9 +24982,11 @@
24952
24982
  const cellRenderer = function (rowElement, column, value) {
24953
24983
  if (window.metadata.columns[column]?.links?.length > 0) {
24954
24984
  const linksColumnElement = document.createElement('td');
24955
- window.metadata.columns[column].links.forEach((link) => {
24956
- linksColumnElement.appendChild(createLink(link, value));
24957
- });
24985
+ if (value) {
24986
+ window.metadata.columns[column].links.forEach((link) => {
24987
+ linksColumnElement.appendChild(createLink(link, value));
24988
+ });
24989
+ }
24958
24990
  rowElement.appendChild(linksColumnElement);
24959
24991
  const textColumnElement = document.createElement('td');
24960
24992
  textColumnElement.innerText = value;
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sqlui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.51
4
+ version: 0.1.52
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-28 00:00:00.000000000 Z
11
+ date: 2022-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: airbrake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: mysql2
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -24,6 +38,20 @@ dependencies:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
40
  version: '0.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: prometheus-client
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: sinatra
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -141,10 +169,10 @@ files:
141
169
  - app/sqlui_config.rb
142
170
  - app/views/databases.erb
143
171
  - app/views/error.erb
172
+ - app/views/sqlui.erb
144
173
  - bin/sqlui
145
174
  - client/resources/favicon.svg
146
175
  - client/resources/sqlui.css
147
- - client/resources/sqlui.html
148
176
  - client/resources/sqlui.js
149
177
  homepage: https://github.com/nicholasdower/sqlui
150
178
  licenses: