ch-client 0.0.1
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 +7 -0
- data/.codeclimate.yml +22 -0
- data/.gitignore +9 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.md +58 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.md +262 -0
- data/Rakefile +15 -0
- data/VERSION +1 -0
- data/bin/clickhouse +9 -0
- data/clickhouse.gemspec +36 -0
- data/lib/clickhouse.rb +60 -0
- data/lib/clickhouse/cli.rb +46 -0
- data/lib/clickhouse/cli/client.rb +149 -0
- data/lib/clickhouse/cli/console.rb +73 -0
- data/lib/clickhouse/cli/server.rb +37 -0
- data/lib/clickhouse/cli/server/assets/css/clickhouse.css +177 -0
- data/lib/clickhouse/cli/server/assets/css/codemirror.css +341 -0
- data/lib/clickhouse/cli/server/assets/css/datatables.css +1 -0
- data/lib/clickhouse/cli/server/assets/css/normalize.css +427 -0
- data/lib/clickhouse/cli/server/assets/css/skeleton.css +418 -0
- data/lib/clickhouse/cli/server/assets/js/clickhouse.js +188 -0
- data/lib/clickhouse/cli/server/assets/js/codemirror.js +9096 -0
- data/lib/clickhouse/cli/server/assets/js/datatables.js +166 -0
- data/lib/clickhouse/cli/server/assets/js/disableswipeback.js +97 -0
- data/lib/clickhouse/cli/server/assets/js/jquery.js +11015 -0
- data/lib/clickhouse/cli/server/assets/js/sql.js +232 -0
- data/lib/clickhouse/cli/server/views/index.erb +46 -0
- data/lib/clickhouse/cluster.rb +43 -0
- data/lib/clickhouse/connection.rb +42 -0
- data/lib/clickhouse/connection/client.rb +135 -0
- data/lib/clickhouse/connection/logger.rb +12 -0
- data/lib/clickhouse/connection/query.rb +160 -0
- data/lib/clickhouse/connection/query/result_row.rb +36 -0
- data/lib/clickhouse/connection/query/result_set.rb +103 -0
- data/lib/clickhouse/connection/query/table.rb +50 -0
- data/lib/clickhouse/error.rb +18 -0
- data/lib/clickhouse/utils.rb +23 -0
- data/lib/clickhouse/version.rb +7 -0
- data/script/console +58 -0
- data/test/test_helper.rb +15 -0
- data/test/test_helper/coverage.rb +16 -0
- data/test/test_helper/minitest.rb +13 -0
- data/test/test_helper/simple_connection.rb +12 -0
- data/test/unit/connection/query/test_result_row.rb +36 -0
- data/test/unit/connection/query/test_result_set.rb +196 -0
- data/test/unit/connection/query/test_table.rb +39 -0
- data/test/unit/connection/test_client.rb +206 -0
- data/test/unit/connection/test_cluster.rb +81 -0
- data/test/unit/connection/test_logger.rb +35 -0
- data/test/unit/connection/test_query.rb +410 -0
- data/test/unit/test_clickhouse.rb +99 -0
- data/test/unit/test_connection.rb +55 -0
- data/test/unit/test_utils.rb +39 -0
- metadata +326 -0
data/lib/clickhouse.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require "uri"
|
2
|
+
require "forwardable"
|
3
|
+
require "csv"
|
4
|
+
require "json"
|
5
|
+
require "time"
|
6
|
+
|
7
|
+
require "faraday"
|
8
|
+
require "pond"
|
9
|
+
require "active_support/dependencies/autoload"
|
10
|
+
require "active_support/number_helper"
|
11
|
+
require "active_support/core_ext/string/inflections"
|
12
|
+
|
13
|
+
require "clickhouse/cluster"
|
14
|
+
require "clickhouse/connection"
|
15
|
+
require "clickhouse/utils"
|
16
|
+
require "clickhouse/error"
|
17
|
+
require "clickhouse/version"
|
18
|
+
|
19
|
+
module Clickhouse
|
20
|
+
|
21
|
+
def self.logger=(logger)
|
22
|
+
@logger = logger
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.logger
|
26
|
+
@logger if instance_variables.include?(:@logger)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.configurations=(configurations)
|
30
|
+
@configurations = configurations.inject({}){|h, (k, v)| h[k.to_s] = v; h}
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.configurations
|
34
|
+
@configurations if instance_variables.include?(:@configurations)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.establish_connection(arg = {})
|
38
|
+
config = arg.is_a?(Hash) ? arg : (configurations || {})[arg.to_s]
|
39
|
+
if config
|
40
|
+
connect!(config)
|
41
|
+
else
|
42
|
+
raise InvalidConnectionError, "Invalid connection specified: #{arg.inspect}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.connect!(config = {})
|
47
|
+
@connection = connect(config)
|
48
|
+
@connection.connect!
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.connect(config = {})
|
52
|
+
klass = (config[:urls] || config["urls"]) ? Cluster : Connection
|
53
|
+
klass.new(config)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.connection
|
57
|
+
@connection if instance_variables.include?(:@connection)
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "thor"
|
2
|
+
require "launchy"
|
3
|
+
require "clickhouse"
|
4
|
+
|
5
|
+
module Clickhouse
|
6
|
+
class CLI < Thor
|
7
|
+
|
8
|
+
DEFAULT_URLS = "http://localhost:8123"
|
9
|
+
|
10
|
+
desc "server [HOSTS]", "Start a Sinatra server as ClickHouse client (HOSTS should be comma separated URIs)"
|
11
|
+
method_options [:port, "-p"] => 1982, [:username, "-u"] => :string, [:password, "-P"] => :string
|
12
|
+
def server(urls = DEFAULT_URLS)
|
13
|
+
run! :server, urls, options do
|
14
|
+
Launchy.open "http://localhost:#{options[:port]}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "console [HOSTS]", "Start a Pry console as ClickHouse client (HOSTS should be comma separated URIs)"
|
19
|
+
method_options [:username, "-u"] => :string, [:password, "-P"] => :string
|
20
|
+
def console(urls = DEFAULT_URLS)
|
21
|
+
run! :console, urls, options
|
22
|
+
end
|
23
|
+
|
24
|
+
map "s" => :server
|
25
|
+
map "c" => :console
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def run!(const, urls, options, &block)
|
30
|
+
require_relative "cli/client"
|
31
|
+
require_relative "cli/#{const}"
|
32
|
+
connect! urls, options
|
33
|
+
self.class.const_get(const.to_s.capitalize).run!(:port => options["port"], &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def connect!(urls, options)
|
37
|
+
config = options.merge(:urls => urls.split(",")).inject({}){|h, (k, v)| h[k.to_sym] = v; h}
|
38
|
+
Clickhouse.establish_connection config
|
39
|
+
end
|
40
|
+
|
41
|
+
def method_missing(method, *_args)
|
42
|
+
raise Error, "Unrecognized command \"#{method}\". Please consult `clickhouse help`."
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module Clickhouse
|
2
|
+
class CLI < Thor
|
3
|
+
module Client
|
4
|
+
|
5
|
+
HISTORY_FILE = "#{ENV["HOME"]}/.clickhouse_history"
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
extended(base)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.extended(base)
|
12
|
+
Clickhouse.logger = self
|
13
|
+
load_history
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.debug(message = nil)
|
17
|
+
@log = message.split("\n").detect{|line| line.include?("36m")}
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.log
|
21
|
+
@log
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.load_history
|
25
|
+
File.readlines(HISTORY_FILE).each do |line|
|
26
|
+
Readline::HISTORY.push line.gsub(";;", "\n").strip
|
27
|
+
end if File.exists?(HISTORY_FILE)
|
28
|
+
end
|
29
|
+
|
30
|
+
def alter_history(sql, current = true)
|
31
|
+
(Readline::HISTORY.to_a.count{|line| line[-1] != ";"} + (current ? 1 : 0)).times do
|
32
|
+
Readline::HISTORY.pop
|
33
|
+
end
|
34
|
+
unless Readline::HISTORY.to_a[-1] == sql
|
35
|
+
Readline::HISTORY.push(sql)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def dump_history
|
40
|
+
File.open(HISTORY_FILE, "w+") do |file|
|
41
|
+
Readline::HISTORY.each do |line|
|
42
|
+
file.puts line.strip.gsub("\n", ";;")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def prettify(sql)
|
48
|
+
sql, replaced = numerize_patterns(sql)
|
49
|
+
|
50
|
+
preserved_words = %w(
|
51
|
+
USE
|
52
|
+
SHOW
|
53
|
+
DATABASES
|
54
|
+
TABLES
|
55
|
+
PROCESSLIST
|
56
|
+
INSERT
|
57
|
+
INTO
|
58
|
+
FORMAT
|
59
|
+
SELECT
|
60
|
+
COUNT
|
61
|
+
DISTINCT
|
62
|
+
SAMPLE
|
63
|
+
AS
|
64
|
+
FROM
|
65
|
+
JOIN
|
66
|
+
UNION
|
67
|
+
ALL
|
68
|
+
PREWHERE
|
69
|
+
WHERE
|
70
|
+
AND
|
71
|
+
OR
|
72
|
+
NOT
|
73
|
+
IN
|
74
|
+
GROUP
|
75
|
+
BY
|
76
|
+
HAVING
|
77
|
+
ORDER
|
78
|
+
LIMIT
|
79
|
+
CREATE
|
80
|
+
DESCRIBE
|
81
|
+
ALTER
|
82
|
+
RENAME
|
83
|
+
DROP
|
84
|
+
DETACH
|
85
|
+
ATTACH
|
86
|
+
TABLE
|
87
|
+
VIEW
|
88
|
+
PARTITION
|
89
|
+
EXISTS
|
90
|
+
SET
|
91
|
+
OPTIMIZE
|
92
|
+
WITH
|
93
|
+
TOTALS
|
94
|
+
).sort{|a, b| [b.size, a] <=> [a.size, b]}
|
95
|
+
|
96
|
+
sql.gsub!(/(\b)(#{preserved_words.join("|")})(\b)/i) do
|
97
|
+
"#{$1}#{$2.upcase}#{$3}"
|
98
|
+
end
|
99
|
+
|
100
|
+
interpolate_patterns(sql, replaced)
|
101
|
+
end
|
102
|
+
|
103
|
+
def numerize_patterns(sql, replaced = [])
|
104
|
+
sql = sql.gsub(/(["'])(?:(?=(\\?))\2.)*?\1/) do |match|
|
105
|
+
replaced << match
|
106
|
+
"${#{replaced.size - 1}}"
|
107
|
+
end
|
108
|
+
|
109
|
+
parenthesized = false
|
110
|
+
|
111
|
+
sql = sql.gsub(/\([^\(\)]*?\)/) do |match|
|
112
|
+
parenthesized = true
|
113
|
+
replaced << match
|
114
|
+
"%{#{replaced.size - 1}}"
|
115
|
+
end
|
116
|
+
|
117
|
+
parenthesized ? numerize_patterns(sql, replaced) : [sql, replaced]
|
118
|
+
end
|
119
|
+
|
120
|
+
def interpolate_patterns(sql, replaced)
|
121
|
+
matched = false
|
122
|
+
|
123
|
+
sql = sql.gsub(/(\$|%)\{(\d+)\}/) do |match|
|
124
|
+
matched = true
|
125
|
+
replaced[$2.to_i]
|
126
|
+
end
|
127
|
+
|
128
|
+
matched ? interpolate_patterns(sql, replaced) : sql
|
129
|
+
end
|
130
|
+
|
131
|
+
def execute(sql, &block)
|
132
|
+
if sql[-1] == ";"
|
133
|
+
dump_history
|
134
|
+
method = sql.match(/^(SELECT|SHOW|DESCRIBE)/i) ? :query : :execute
|
135
|
+
result = Clickhouse.connection.send(method, sql[0..-2])
|
136
|
+
|
137
|
+
if block_given?
|
138
|
+
block.call(result, Client.log)
|
139
|
+
else
|
140
|
+
process_result(result, Client.log)
|
141
|
+
end
|
142
|
+
else
|
143
|
+
sql
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "readline"
|
2
|
+
|
3
|
+
module Clickhouse
|
4
|
+
class CLI < Thor
|
5
|
+
module Console
|
6
|
+
extend self
|
7
|
+
extend Client
|
8
|
+
|
9
|
+
CLR = "\r\e[A\e[K"
|
10
|
+
|
11
|
+
def run!(options = {})
|
12
|
+
readline
|
13
|
+
end
|
14
|
+
|
15
|
+
# private
|
16
|
+
|
17
|
+
def readline(buffer = nil)
|
18
|
+
prompt = buffer ? ":-] " : ":) "
|
19
|
+
line = Readline.readline(prompt, true)
|
20
|
+
|
21
|
+
exit! unless line && !%w(exit quit).include?(line = line.strip)
|
22
|
+
|
23
|
+
line = prettify(line)
|
24
|
+
sql = [buffer, line].compact.join("\n").gsub(/\s+;$/, ";")
|
25
|
+
|
26
|
+
puts "#{CLR}#{prompt}#{line}"
|
27
|
+
alter_history(sql)
|
28
|
+
|
29
|
+
buffer = begin
|
30
|
+
execute(sql)
|
31
|
+
rescue Clickhouse::Error => e
|
32
|
+
puts "ERROR: #{e.message}"
|
33
|
+
end
|
34
|
+
|
35
|
+
readline buffer
|
36
|
+
end
|
37
|
+
|
38
|
+
def process_result(result, log)
|
39
|
+
if result.is_a?(Clickhouse::Connection::Query::ResultSet)
|
40
|
+
if result.size > 0
|
41
|
+
array = [result.names].concat(result.to_a)
|
42
|
+
lengths = array.inject([]) do |lengths, row|
|
43
|
+
row.each_with_index do |value, index|
|
44
|
+
length = value.to_s.strip.length
|
45
|
+
lengths[index] = [lengths[index].to_i, length].max
|
46
|
+
end
|
47
|
+
lengths
|
48
|
+
end
|
49
|
+
puts
|
50
|
+
array.each_with_index do |row, i|
|
51
|
+
values = [nil]
|
52
|
+
lengths.each_with_index do |length, index|
|
53
|
+
values << row[index].to_s.ljust(length, " ")
|
54
|
+
end
|
55
|
+
values << nil
|
56
|
+
separator = (i == 0) ? "+" : "|"
|
57
|
+
puts values.join(" #{separator} ")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
else
|
61
|
+
puts result == true ? "Ok." : (result || "Fail.")
|
62
|
+
end
|
63
|
+
|
64
|
+
if log
|
65
|
+
puts
|
66
|
+
puts log.strip
|
67
|
+
end
|
68
|
+
puts
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "sinatra"
|
2
|
+
require "erubis"
|
3
|
+
|
4
|
+
module Clickhouse
|
5
|
+
class CLI < Thor
|
6
|
+
class Server < Sinatra::Base
|
7
|
+
include Client
|
8
|
+
|
9
|
+
set :views, File.expand_path("../server/views", __FILE__)
|
10
|
+
set :public_folder, File.expand_path("../server/assets", __FILE__)
|
11
|
+
|
12
|
+
get "/" do
|
13
|
+
erb :index
|
14
|
+
end
|
15
|
+
|
16
|
+
post "/" do
|
17
|
+
sql = prettify(params[:sql]).gsub(/\s+;$/, ";")
|
18
|
+
alter_history(sql, false)
|
19
|
+
begin
|
20
|
+
execute(sql) do |result, log|
|
21
|
+
content_type :json
|
22
|
+
{
|
23
|
+
:urls => Clickhouse.connection.pond.available.collect(&:url),
|
24
|
+
:history => Readline::HISTORY.to_a.collect(&:strip),
|
25
|
+
:names => result.names,
|
26
|
+
:data => result.to_a,
|
27
|
+
:stats => log.sub("\e[1m\e[36m", "").sub("\e[0m", "").strip
|
28
|
+
}.to_json
|
29
|
+
end
|
30
|
+
rescue Clickhouse::Error => e
|
31
|
+
halt 500, e.message
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
@import url('https://fonts.googleapis.com/css?family=Inconsolata|Open+Sans');
|
2
|
+
|
3
|
+
body {
|
4
|
+
font-family: 'Open Sans', sans-serif;
|
5
|
+
font-size: 1.2rem;
|
6
|
+
}
|
7
|
+
|
8
|
+
h1, h2, h3, h4, h5, h6 {
|
9
|
+
margin-bottom: 1.1rem;
|
10
|
+
}
|
11
|
+
|
12
|
+
h6 {
|
13
|
+
font-size: 1.75rem;
|
14
|
+
font-weight: 600;
|
15
|
+
}
|
16
|
+
|
17
|
+
.header {
|
18
|
+
padding: 5px 17px;
|
19
|
+
background: #F3F3F3;
|
20
|
+
border-bottom: 1px solid #E0E0E0;
|
21
|
+
}
|
22
|
+
|
23
|
+
.container {
|
24
|
+
width: calc(100% - 36px);
|
25
|
+
max-width: inherit;
|
26
|
+
}
|
27
|
+
|
28
|
+
.not_connected {
|
29
|
+
color: #D74D2F;
|
30
|
+
}
|
31
|
+
|
32
|
+
small {
|
33
|
+
font-size: 1.2rem;
|
34
|
+
}
|
35
|
+
|
36
|
+
a {
|
37
|
+
text-decoration: none;
|
38
|
+
}
|
39
|
+
|
40
|
+
a:hover {
|
41
|
+
text-decoration: underline;
|
42
|
+
}
|
43
|
+
|
44
|
+
form {
|
45
|
+
margin-bottom: 7px;
|
46
|
+
}
|
47
|
+
|
48
|
+
textarea {
|
49
|
+
opacity: 0;
|
50
|
+
}
|
51
|
+
|
52
|
+
textarea, .CodeMirror {
|
53
|
+
margin-bottom: 1.3em;
|
54
|
+
width: 100%;
|
55
|
+
height: 9.1em;
|
56
|
+
display: block;
|
57
|
+
font-family: 'Inconsolata';
|
58
|
+
font-size: 1.2rem;
|
59
|
+
font-weight: 600;
|
60
|
+
line-height: 1.75rem;
|
61
|
+
letter-spacing: 0;
|
62
|
+
border: 1px solid #DDD;
|
63
|
+
}
|
64
|
+
|
65
|
+
input[type="submit"], a.download {
|
66
|
+
height: 30px;
|
67
|
+
margin-right: 4px;
|
68
|
+
padding: 0 17px 0 20px;
|
69
|
+
color: white;
|
70
|
+
font-size: 10px;
|
71
|
+
line-height: 28px;
|
72
|
+
letter-spacing: .05rem;
|
73
|
+
border-radius: 0;
|
74
|
+
}
|
75
|
+
|
76
|
+
input[type="submit"] {
|
77
|
+
background: #D74D2F;
|
78
|
+
border-color: #D74D2F;
|
79
|
+
}
|
80
|
+
|
81
|
+
a.download {
|
82
|
+
background: #999;
|
83
|
+
border-color: #999;
|
84
|
+
}
|
85
|
+
|
86
|
+
input[type="submit"]:hover, input[type="submit"]:focus {
|
87
|
+
color: white;
|
88
|
+
border-color: #B30015;
|
89
|
+
}
|
90
|
+
|
91
|
+
a.download:hover, a.download:focus {
|
92
|
+
color: white;
|
93
|
+
text-decoration: none;
|
94
|
+
}
|
95
|
+
|
96
|
+
input[disabled="disabled"],a[disabled="disabled"] {
|
97
|
+
color: #F9F9F9;
|
98
|
+
cursor: default !important;
|
99
|
+
background: #E4E4E4;
|
100
|
+
border-color: #E7E7E7 !important;
|
101
|
+
}
|
102
|
+
|
103
|
+
#stats {
|
104
|
+
padding-left: 1.5rem;
|
105
|
+
}
|
106
|
+
|
107
|
+
#result_wrapper {
|
108
|
+
margin-bottom: 10px;
|
109
|
+
padding-top: 18px;
|
110
|
+
overflow-x: auto;
|
111
|
+
border-top: 1px solid #DDD;
|
112
|
+
}
|
113
|
+
|
114
|
+
#result_filter {
|
115
|
+
float: left;
|
116
|
+
}
|
117
|
+
|
118
|
+
#result_filter label {
|
119
|
+
padding-left: 1px;
|
120
|
+
font-family: 'Helvetica Neue', 'Arial';
|
121
|
+
font-weight: bold;
|
122
|
+
}
|
123
|
+
|
124
|
+
#result_filter input[type="search"] {
|
125
|
+
width: 300px;
|
126
|
+
height: 28px;
|
127
|
+
margin-left: 12px;
|
128
|
+
margin-bottom: 12px;
|
129
|
+
padding: 6px 7px;
|
130
|
+
font-weight: normal;
|
131
|
+
border-radius: 0;
|
132
|
+
}
|
133
|
+
|
134
|
+
#result_filter input[type="search"]:focus {
|
135
|
+
border-color: #999;
|
136
|
+
}
|
137
|
+
|
138
|
+
#result {
|
139
|
+
width: 100% !important;
|
140
|
+
margin-bottom: 15px;
|
141
|
+
font-family: 'Helvetica Neue', 'Arial';
|
142
|
+
font-size: 1.25rem;
|
143
|
+
}
|
144
|
+
|
145
|
+
table#result {
|
146
|
+
border: 0;
|
147
|
+
border-top: 2px solid #111;
|
148
|
+
border-left: 1px solid #DDD;
|
149
|
+
}
|
150
|
+
|
151
|
+
#result th, #result td {
|
152
|
+
padding: 4px 8px;
|
153
|
+
border: 0;
|
154
|
+
border-right: 1px solid #DDD;
|
155
|
+
border-bottom: 1px solid #DDD;
|
156
|
+
}
|
157
|
+
|
158
|
+
#result th {
|
159
|
+
background: #EEE;
|
160
|
+
}
|
161
|
+
|
162
|
+
#result td {
|
163
|
+
white-space: nowrap;
|
164
|
+
}
|
165
|
+
|
166
|
+
#result td.odd {
|
167
|
+
background: #DDD;
|
168
|
+
}
|
169
|
+
|
170
|
+
#result td.even {
|
171
|
+
background: #FFF;
|
172
|
+
}
|
173
|
+
|
174
|
+
#result_info {
|
175
|
+
height: 3px;
|
176
|
+
display: none;
|
177
|
+
}
|