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 +4 -4
- data/.version +1 -1
- data/app/server.rb +72 -31
- data/app/sqlui_config.rb +2 -1
- data/app/views/databases.erb +2 -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:
|