sqlui 0.1.50 → 0.1.52

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: 347f6283d12953312d396c75f65e0788f82c305bb86be418ff2afee80a177499
4
- data.tar.gz: b90813712a41a529531f3bd29ca25b706d94adaf1c9fbe932663a289cff16ae4
3
+ metadata.gz: 67496893776f87c67e14c32003e03f5ad2efa2dede7afc7efb6b7d69a6489fcc
4
+ data.tar.gz: c7b0678eada7efa6b22666950b16157e5dfd06bebee46b978e2a8c2339a456e0
5
5
  SHA512:
6
- metadata.gz: 3380c451cb5496090c00581a6a97231dd14ab10c9b470bb8a3858b2bc8afe8719c9698c43f2c92b4b99048c93a2527ec0452d5f94ed8ca16885a7d257e23b926
7
- data.tar.gz: ec1a2d815cc9385f2d79b1ddde8410a7991bf629faeb8a75954b8a75d9c56d3a192e5ae41c343820867f9a2607a1319e05c7a4dbbeb2b09d9e0ef2c3d04b66a0
6
+ metadata.gz: 23df6f8e5acdff2a680e933db3730f989d8e4f6cc51829f091c7aa12651a27ab3179eac4a31eae3d6f4dbd314000628dc0deca94c553f359f599e2a44d25bfbe
7
+ data.tar.gz: 3389a5572cddc8ebe39b7d12eb7fef3d1548aae2a2957b0eeea96fa18e622acc204eb85bc1aec68556757058ac3142b5471e1001a61678a124a7fd55c36a2154
data/.version CHANGED
@@ -1 +1 @@
1
- 0.1.50
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,7 +1,9 @@
1
+ <!DOCTYPE html>
1
2
  <html lang="en">
2
3
  <head>
3
4
  <meta charset="utf-8">
4
5
  <title>SQLUI <%= config.name %> Databases</title>
6
+ <link rel="icon" type="image/x-icon" href="/favicon.svg">
5
7
 
6
8
  <style>
7
9
  body {
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.50
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: