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 +4 -4
- data/.version +1 -1
- data/app/server.rb +72 -31
- data/app/sqlui_config.rb +2 -1
- data/app/views/databases.erb +1 -0
- data/app/views/error.erb +1 -0
- data/{client/resources/sqlui.html → app/views/sqlui.erb} +16 -1
- data/client/resources/sqlui.css +3 -0
- data/client/resources/sqlui.js +95 -63
- metadata +31 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67496893776f87c67e14c32003e03f5ad2efa2dede7afc7efb6b7d69a6489fcc
|
4
|
+
data.tar.gz: c7b0678eada7efa6b22666950b16157e5dfd06bebee46b978e2a8c2339a456e0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23df6f8e5acdff2a680e933db3730f989d8e4f6cc51829f091c7aa12651a27ab3179eac4a31eae3d6f4dbd314000628dc0deca94c553f359f599e2a44d25bfbe
|
7
|
+
data.tar.gz: 3389a5572cddc8ebe39b7d12eb7fef3d1548aae2a2957b0eeea96fa18e622acc204eb85bc1aec68556757058ac3142b5471e1001a61678a124a7fd55c36a2154
|
data/.version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
132
|
-
|
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
|
-
|
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
|
-
|
167
|
-
|
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:)
|
data/app/views/databases.erb
CHANGED
data/app/views/error.erb
CHANGED
@@ -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
|
-
|
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>
|
data/client/resources/sqlui.css
CHANGED
data/client/resources/sqlui.js
CHANGED
@@ -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
|
-
|
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
|
-
|
23938
|
-
|
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
|
-
|
23946
|
-
|
23947
|
-
|
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:
|
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
|
-
|
23996
|
-
const
|
24014
|
+
|
24015
|
+
const joinCompletions = [];
|
23997
24016
|
metadata.joins.forEach((join) => {
|
23998
|
-
|
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
|
-
|
24003
|
-
|
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
|
24029
|
-
|
24030
|
-
//
|
24031
|
-
//
|
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
|
24039
|
-
// statement in the editor (semicolon present). In that case we want to find the node just before
|
24040
|
-
// current position
|
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
|
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
|
24050
|
-
//
|
24051
|
-
//
|
24052
|
-
//
|
24053
|
-
//
|
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
|
-
|
24073
|
-
|
24074
|
-
|
24075
|
-
|
24076
|
-
|
24077
|
-
|
24078
|
-
|
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.
|
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:
|
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
|
-
|
24956
|
-
|
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.
|
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
|
+
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:
|