hieraviz 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +40 -0
  5. data/app/apiv1.rb +58 -0
  6. data/app/common.rb +22 -0
  7. data/app/config/hieraviz.default.yml +13 -0
  8. data/app/config/hieraviz.yml +13 -0
  9. data/app/main.rb +5 -0
  10. data/app/public/css/main.css +241 -0
  11. data/app/public/img/loader.gif +0 -0
  12. data/app/public/js/farms.js +60 -0
  13. data/app/public/js/logout.js +13 -0
  14. data/app/public/js/main.js +98 -0
  15. data/app/public/js/modules.js +40 -0
  16. data/app/public/js/nodes.js +145 -0
  17. data/app/public/js/resources.js +40 -0
  18. data/app/views/_foot.erb +3 -0
  19. data/app/views/_head.erb +14 -0
  20. data/app/views/_layout.erb +24 -0
  21. data/app/views/farms.erb +16 -0
  22. data/app/views/home.erb +3 -0
  23. data/app/views/logout.erb +7 -0
  24. data/app/views/modules.erb +8 -0
  25. data/app/views/nodes.erb +16 -0
  26. data/app/views/not_found.erb +3 -0
  27. data/app/views/resources.erb +8 -0
  28. data/app/web.rb +81 -0
  29. data/bin/hv-ctl +3 -0
  30. data/config.ru +8 -0
  31. data/lib/hieraviz/store.rb +17 -0
  32. data/lib/hieraviz/version.rb +3 -0
  33. data/lib/hieraviz.rb +6 -0
  34. data/spec/app/apiv1_spec.rb +12 -0
  35. data/spec/app/web_spec.rb +12 -0
  36. data/spec/files/config.yml +12 -0
  37. data/spec/files/hiera.yml +10 -0
  38. data/spec/files/puppet/enc/node1.example.com.yaml +7 -0
  39. data/spec/files/puppet/farm_modules/farm1/manifests/init.pp +3 -0
  40. data/spec/files/puppet/hiera.yml +10 -0
  41. data/spec/files/puppet/modules/module1/init.pp +2 -0
  42. data/spec/files/puppet/params/common/common.yml +2 -0
  43. data/spec/files/puppet/params/nodes/node1.example.com.yaml +2 -0
  44. data/spec/spec_helper.rb +23 -0
  45. metadata +257 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6c1283ce82b3cdaba2917c8bd1f2d6606f6272b5
4
+ data.tar.gz: 789132864aef111d8cbc4c1d5dc4b8478ac1aa60
5
+ SHA512:
6
+ metadata.gz: 856f47c9edd3e9333bc8e795258e916935be940d6ba216c72aba7e70ffa280f1a459dda96601c0ea3c15e0fdd46aa6bf3913fcd0ea9955c39204264b0759c968
7
+ data.tar.gz: 559f5c324a0db557223502df3f302ed9399e547a797790737da0946e90ecbd2c898c9718eb1d61b79a26ca6a01dc19e32ff6be9c2153a6809bf2c4851cd36784
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ Hieraviz Changelog
2
+ ========================
3
+
4
+ ### v0.0.1 - 2015-12-30
5
+ - still pretty alpha at this stage, more to come soon
6
+ - made basic auth work (with logout too)
7
+ - grab information for nodes
8
+ - basic sinatra architecture
9
+ - initial setup
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 gandi.net
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ Hieraviz
2
+ ===============
3
+
4
+ [![Gem Version](https://img.shields.io/gem/v/hieraviz.svg)](http://rubygems.org/gems/hieraviz)
5
+ [![Downloads](http://img.shields.io/gem/dt/hieraviz.svg)](https://rubygems.org/gems/hieraviz)
6
+ [![Build Status](https://img.shields.io/travis/Gandi/hieraviz.svg)](https://travis-ci.org/Gandi/hieraviz)
7
+ [![Coverage Status](https://img.shields.io/coveralls/Gandi/hieraviz.svg)](https://coveralls.io/github/Gandi/hieraviz?branch=master)
8
+ [![Dependency Status](https://gemnasium.com/Gandi/hieraviz.svg)](https://gemnasium.com/Gandi/hieraviz)
9
+ [![Code Climate](https://img.shields.io/codeclimate/github/Gandi/hieraviz.svg)](https://codeclimate.com/github/Gandi/hieraviz)
10
+
11
+
12
+ Hieraviz is a simple web application for accessing Puppet development code and production data in a unified interface. Its main goal is to enable a better visibility on the Puppet architecture for more actors to be able to interact with it.
13
+
14
+ It's currently in very early stages of development, use at your own risk.
15
+
16
+ Installation
17
+ -------------------
18
+ Install it from Rubygens:
19
+
20
+ gem install hieraviz
21
+
22
+ Usage
23
+ ----------
24
+ `hv-ctl` is a control script to manipulate Hieraviz server. It is not written yet, unfortunately, but will be ready in the very next versions.
25
+
26
+ Contributing
27
+ ----------------
28
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Gandi/hieraviz.
29
+
30
+ Authors
31
+ -----------
32
+ Hieracles original code is written by [@mose](https://github.com/mose).
33
+
34
+ License
35
+ -----------
36
+ Hieraviz is available under [MIT License](http://opensource.org/licenses/MIT).
37
+
38
+ Copyright
39
+ ------------
40
+ copyright (c) 2015, 2016 Gandi http://gandi.net
data/app/apiv1.rb ADDED
@@ -0,0 +1,58 @@
1
+ require 'sinatra/json'
2
+
3
+ require 'digest/sha1'
4
+ require 'dotenv'
5
+ require 'yajl'
6
+
7
+ require 'hieracles'
8
+ require 'hieraviz'
9
+
10
+ require File.expand_path '../common.rb', __FILE__
11
+
12
+ module HieravizApp
13
+ class ApiV1 < Common
14
+
15
+ get '/test' do
16
+ json data: Time.new
17
+ end
18
+
19
+ get '/nodes' do
20
+ json Hieracles::Registry.nodes(settings.config)
21
+ end
22
+
23
+ get '/node/:n/info' do |node|
24
+ node = Hieracles::Node.new(node, settings.config)
25
+ json node.info
26
+ end
27
+
28
+ get '/node/:n/params' do |node|
29
+ node = Hieracles::Node.new(node, settings.config)
30
+ json node.params
31
+ end
32
+
33
+ get '/node/:n/allparams' do |node|
34
+ node = Hieracles::Node.new(node, settings.config)
35
+ json node.params(false)
36
+ end
37
+
38
+ get '/node/:n' do |node|
39
+ node = Hieracles::Node.new(node, settings.config)
40
+ json node.params
41
+ end
42
+
43
+ get '/farms' do
44
+ json Hieracles::Registry.farms(settings.config)
45
+ end
46
+
47
+ get '/farm/:n' do |farm|
48
+ req = Hieracles::Puppetdb::Request.new(settings.configdata['puppetdb'])
49
+ farm_nodes = req.facts('farm', farm)
50
+ json farm_nodes.data
51
+ end
52
+
53
+ not_found do
54
+ json({ error: "data not found" })
55
+ end
56
+
57
+ end
58
+ end
data/app/common.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'sinatra/base'
2
+ require 'dotenv'
3
+ require 'hieracles'
4
+ require 'hieraviz'
5
+
6
+ module HieravizApp
7
+ class Common < Sinatra::Base
8
+
9
+ configure do
10
+ set :app_name, 'HieraViz'
11
+ configfile = ENV['HIERAVIZ_CONFIG_FILE'] || File.join("config", "hieraviz.yml")
12
+ configfile = File.join(root, configfile) unless configfile[0] == '/'
13
+ set :configfile, configfile
14
+ set :configdata, YAML.load_file(configfile)
15
+ set :config, Hieracles::Config.new({ config: configfile })
16
+ enable :session
17
+ enable :logging
18
+ set :store, Hieraviz::Store.new
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ ---
2
+ app_name: HieraViz
3
+ basepath: "../puppet"
4
+ classpath: "farm_modules/%s/manifests/init.pp"
5
+ hierafile: "dev/hiera-local.yaml"
6
+ usedb: true
7
+ puppetdb:
8
+ usessl: false
9
+ host: puppetdb.example.com
10
+ port: 8080
11
+ http_auth:
12
+ username: 'xxx'
13
+ password: 'xxx'
@@ -0,0 +1,13 @@
1
+ ---
2
+ app_name: HieraViz
3
+ basepath: "../../gandi/puppet2.7"
4
+ classpath: "farm_modules/%s/manifests/init.pp"
5
+ hierafile: "dev/hiera-local.yaml"
6
+ usedb: true
7
+ puppetdb:
8
+ usessl: false
9
+ host: puppetdb.sd2.0x35.net
10
+ port: 8080
11
+ http_auth:
12
+ username: 'xxx'
13
+ password: 'xxx'
data/app/main.rb ADDED
@@ -0,0 +1,5 @@
1
+ require File.expand_path '../web.rb', __FILE__
2
+ require File.expand_path '../apiv1.rb', __FILE__
3
+
4
+ module HieravizApp
5
+ end
@@ -0,0 +1,241 @@
1
+ /* general elements */
2
+ * {
3
+ box-sizing: border-box;
4
+ }
5
+ body {
6
+ margin: 0;
7
+ padding: 0;
8
+ background-color: #eee;
9
+ font-family: sans serif;
10
+ }
11
+ a {
12
+ color: #369;
13
+ text-decoration: none;
14
+ }
15
+ a:hover {
16
+ color: #993;
17
+ text-decoration: underline;
18
+ }
19
+ input {
20
+ padding: .1em .2em;
21
+ border: 1px solid #ccc;
22
+ border-radius: .1em;
23
+ background-color: #fff;
24
+ }
25
+ /* Layout elements */
26
+ /* ----- Head ---- */
27
+ .head {
28
+ padding: 1em;
29
+ background-color: #424242;
30
+ color: #fff;
31
+ position: fixed;
32
+ display: block;
33
+ width: 100%;
34
+ z-index: 1;
35
+ }
36
+ .head .title a {
37
+ color: #fff;
38
+ font-size: 2em;
39
+ display: inline-block;
40
+ float: left;
41
+ line-height: .5em;
42
+ margin-right: 1em;
43
+ }
44
+ .head .nav {
45
+ position: relative;
46
+ float: left;
47
+ display: inline-block;
48
+ }
49
+ .head .nav a {
50
+ position: relative;
51
+ color: #fff;
52
+ background-color: #999;
53
+ padding: .5em 1em;
54
+ border-radius: .2em;
55
+ font-weight: bold;
56
+ }
57
+ .head .nav a.focus {
58
+ background-color: #fff;
59
+ color: #000;
60
+ }
61
+ .head .nav a:hover {
62
+ background-color: #fff;
63
+ color: #000;
64
+ }
65
+ .head .auth {
66
+ position: relative;
67
+ display: inline-block;
68
+ float: right;
69
+ }
70
+ .head .auth a {
71
+ color: #fff;
72
+ padding: .5em 1em;
73
+ border-radius: .2em;
74
+ font-weight: bold;
75
+ background-color: #000;
76
+ border-radius: .2em;
77
+ cursor: pointer;
78
+ }
79
+ .head .auth a:hover {
80
+ background-color: #fff;
81
+ color: #000;
82
+ }
83
+ /* ----- Content ---- */
84
+ .content {
85
+ background-color: #eee;
86
+ position: absolute;
87
+ top: 3em;
88
+ bottom: 8em;
89
+ width: 100%;
90
+ }
91
+
92
+ /* ----- Sidebar ---- */
93
+ .side {
94
+ background-color: #eee;
95
+ display: inline-block;
96
+ float: left;
97
+ width: 330px;
98
+ }
99
+ .side .filter {
100
+ margin: 0;
101
+ background-color: #999;
102
+ font-size: 1.2em;
103
+ }
104
+ .side .filter input {
105
+ width: 100%;
106
+ margin-bottom: .1em;
107
+ padding: .1em 1em;
108
+ }
109
+ .side ul {
110
+ margin: 0;
111
+ padding: 0;
112
+ list-style: none;
113
+ border-right: 1px solid #ccc;
114
+ }
115
+ .side ul li {
116
+ padding: .1em 1em;
117
+ cursor: pointer;
118
+ border-bottom: 1px solid #ccc;
119
+ }
120
+ .side ul li:hover {
121
+ background-color: #fff;
122
+ }
123
+ .side ul li.focus {
124
+ background-color: #fff;
125
+ }
126
+
127
+ /* ----- Meat ---- */
128
+ .meat {
129
+ border: 2px solid #999;
130
+ background-color: #fff;
131
+ margin-left: 330px;
132
+ position: relative;
133
+ }
134
+ .meat.text {
135
+ padding: 1em 2em;
136
+ }
137
+ .meat.wait {
138
+ background-color: #ddd;
139
+ position: relative;
140
+ }
141
+ .meat.wait:after {
142
+ content: '\A';
143
+ position: absolute;
144
+ bottom: 0;
145
+ top: 0;
146
+ right: 0;
147
+ left: 0;
148
+ opacity: 0.7;
149
+ transition: all 1s;
150
+ background: #fff url('/img/loader.gif') no-repeat center 200px;
151
+ }
152
+ .meat h3 {
153
+ margin: 0em;
154
+ margin-bottom: 0;
155
+ padding: .2em 1em;
156
+ background-color: #999;
157
+ }
158
+ .meat .nodenav {
159
+ position: absolute;
160
+ top: 0;
161
+ line-height: 1.6em;
162
+ right: 1em;
163
+ }
164
+ .meat .nodenav span {
165
+ font-size: .8em;
166
+ cursor: pointer;
167
+ background-color: #ccc;
168
+ margin-left: 1em;
169
+ padding: 0.2em 1em;
170
+ }
171
+ .meat .nodenav span:hover {
172
+ background-color: #fff;
173
+ }
174
+ .meat .nodenav span.focus {
175
+ background-color: #fff;
176
+ }
177
+ .meat .paramfilter {
178
+ margin: 0;
179
+ padding: 0 1em;
180
+ background-color: #999;
181
+ font-size: 1.2em;
182
+ padding-bottom: .5em;
183
+ }
184
+ .meat .paramfilter input {
185
+ width: 100%;
186
+ margin-bottom: .1em;
187
+ padding: .1em 1em;
188
+ }
189
+ .meat .row {
190
+ word-wrap: break-word;
191
+ }
192
+ .meat .row.overriden {
193
+ margin-left: 3em;
194
+ color: #777;
195
+ font-weight: .8em;
196
+ }
197
+ .meat .row .paramfile {
198
+ font-size: .8em;
199
+ overflow: hidden;
200
+ padding: .28em 1em;
201
+ white-space: nowrap;
202
+ float: right;
203
+ cursor: pointer;
204
+ background: #eee;
205
+ line-height: 1.2em;
206
+ }
207
+ .meat .row .data {
208
+ background-color: #bcd;
209
+ padding: .1em 1em;
210
+ }
211
+ .meat .row .value {
212
+ font-family: monospace;
213
+ padding: .1em 1em .6em;
214
+ margin: 0;
215
+ }
216
+ .meat .row:hover {
217
+ background-color: #f3f3de;
218
+ }
219
+ .meat .row:hover .paramfile {
220
+ background-color: #ee9;
221
+ }
222
+ .meat .row:hover .data {
223
+ background-color: #dd6;
224
+ }
225
+ .meat.farms div {
226
+ padding: .5em 1em;
227
+ }
228
+ .meat.farms div:hover {
229
+ background-color: #f3f3de;
230
+ }
231
+ /* ----- Foot ---- */
232
+ .foot {
233
+ padding: 1em;
234
+ background-color: #424242;
235
+ color: #fff;
236
+ position: fixed;
237
+ display: block;
238
+ bottom: 0;
239
+ line-height: 1em;
240
+ width: 100%;
241
+ }
Binary file
@@ -0,0 +1,60 @@
1
+ /*
2
+ We don't need jQuery fat mama
3
+ http://youmightnotneedjquery.com/
4
+ https://github.com/oneuijs/You-Dont-Need-jQuery
5
+
6
+ also
7
+ We don't need to care about freaking IE
8
+ let's use the fetch API for ajax calls
9
+ https://fetch.spec.whatwg.org
10
+ */
11
+
12
+ function ready(fn) {
13
+ if (document.readyState != 'loading') {
14
+ fn();
15
+ } else {
16
+ document.addEventListener('DOMContentLoaded', fn);
17
+ }
18
+ }
19
+
20
+ ready( () => {
21
+ focusNav('farms');
22
+
23
+ var farms = document.querySelectorAll('li.farm');
24
+ var meat = document.querySelector('div.meat');
25
+
26
+ filterBox(".filter input", farms);
27
+
28
+ function build_list(top, title, array) {
29
+ window.location.hash = '#'+title;
30
+ top.innerHTML = "<h3>Farm "+title+"</h3>";
31
+ if (array.length > 0)
32
+ Array.prototype.forEach.call(array, (item, i) => {
33
+ addTo(top, "<div><a href=\"/nodes#"+ item +"\">" +
34
+ item +
35
+ "</a></div>\n");
36
+ });
37
+ else
38
+ addTo(top, "<div>There is no node in this farm.</div>\n");
39
+ }
40
+
41
+ Array.prototype.forEach.call(farms, (item, i) => {
42
+ item.addEventListener('click', (ev) => {
43
+ addClass(meat, 'wait');
44
+ el = ev.target;
45
+ fetch('/v1/farm/' + el.innerText).
46
+ then(res => res.json()).
47
+ then(j => {
48
+ build_list(meat, el.innerText, j);
49
+ Array.prototype.forEach.call(farms, (item, i) => {
50
+ removeClass(item, 'focus')
51
+ });
52
+ addClass(el, 'focus');
53
+ removeClass(meat, 'wait');
54
+ });
55
+ });
56
+ });
57
+
58
+ restore_url(farms);
59
+
60
+ });
@@ -0,0 +1,13 @@
1
+ ready( () => {
2
+
3
+ fetch('/', {
4
+ credentials: 'include',
5
+ headers: {
6
+ "Authorization": "Basic blah"
7
+ }
8
+ }).then( () => {
9
+ window.location = "/";
10
+ }
11
+ );
12
+
13
+ });
@@ -0,0 +1,98 @@
1
+ /*
2
+ We don't need jQuery fat mama
3
+ http://youmightnotneedjquery.com/
4
+ https://github.com/oneuijs/You-Dont-Need-jQuery
5
+
6
+ also
7
+ We don't need to care about freaking IE
8
+ let's use the fetch API for ajax calls
9
+ https://fetch.spec.whatwg.org
10
+ */
11
+
12
+
13
+ function ready(fn) {
14
+ if (document.readyState != 'loading') {
15
+ fn();
16
+ } else {
17
+ document.addEventListener('DOMContentLoaded', fn);
18
+ }
19
+ }
20
+
21
+ var meat = document.querySelector('div.meat');
22
+
23
+ function make_base_auth(user, password) {
24
+ var tok = user + ':' + password;
25
+ var hash = btoa(tok);
26
+ return "Basic " + hash;
27
+ }
28
+
29
+ function addClass(el, className) {
30
+ if (el.classList)
31
+ el.classList.add(className);
32
+ else
33
+ el.className += ' ' + className;
34
+ }
35
+
36
+ function removeClass(el, className) {
37
+ if (el.classList)
38
+ el.classList.remove(className);
39
+ else
40
+ el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
41
+ }
42
+
43
+ function focusNav(className) {
44
+ var nav = document.querySelectorAll('.nav a');
45
+ Array.prototype.forEach.call(nav, (item, i) => {
46
+ removeClass(item, 'focus')
47
+ });
48
+ var navFocus = document.querySelector('.nav a.' + className);
49
+ addClass(navFocus, 'focus');
50
+ }
51
+
52
+ function addTo(el, txt) {
53
+ el.insertAdjacentHTML("beforeend", txt);
54
+ }
55
+
56
+ function shortParamFile(path) {
57
+ return path.replace(/params\//, '').replace(/\.yaml/, '');
58
+ }
59
+
60
+ function filterBox(input, els) {
61
+ var filterinput = document.querySelector(input);
62
+ filterinput.focus();
63
+ filterinput.addEventListener('keyup', (ev) => {
64
+ el = ev.target;
65
+ if (el.value == '')
66
+ Array.prototype.forEach.call(els, (item, i) => {
67
+ item.style.display = 'block';
68
+ });
69
+ else
70
+ Array.prototype.forEach.call(els, (item, i) => {
71
+ if (item.innerText.match(el.value))
72
+ item.style.display = 'block';
73
+ else
74
+ item.style.display = 'none';
75
+ });
76
+ });
77
+ }
78
+
79
+ function start_wait(meat) {
80
+ addClass(meat, 'wait');
81
+ }
82
+
83
+ function end_wait(meat) {
84
+ removeClass(meat, 'wait');
85
+ }
86
+
87
+ function restore_url(list) {
88
+ if (window.location.hash != '') {
89
+ var target = window.location.hash.replace(/#/,'');
90
+ Array.prototype.forEach.call(list, (item, i) => {
91
+ if (item.textContent == target) {
92
+ var event = document.createEvent('HTMLEvents');
93
+ event.initEvent('click', true, false);
94
+ item.dispatchEvent(event);
95
+ }
96
+ });
97
+ }
98
+ }
@@ -0,0 +1,40 @@
1
+ /*
2
+ We don't need jQuery fat mama
3
+ http://youmightnotneedjquery.com/
4
+ https://github.com/oneuijs/You-Dont-Need-jQuery
5
+
6
+ also
7
+ We don't need to care about freaking IE
8
+ let's use the fetch API for ajax calls
9
+ https://fetch.spec.whatwg.org
10
+ */
11
+
12
+ function ready(fn) {
13
+ if (document.readyState != 'loading') {
14
+ fn();
15
+ } else {
16
+ document.addEventListener('DOMContentLoaded', fn);
17
+ }
18
+ }
19
+
20
+ ready( () => {
21
+ focusNav('modules');
22
+
23
+ // var nodes = document.querySelectorAll('li.node');
24
+ // var meat = document.querySelector('pre.meat');
25
+
26
+ // Array.prototype.forEach.call(nodes, (item, i) => {
27
+ // item.addEventListener('click', (ev) => {
28
+ // el = ev.target;
29
+ // fetch('/v1/node/' + el.innerText).
30
+ // then(res => res.json()).
31
+ // then(j => {
32
+ // meat.textContent = JSON.stringify(j);
33
+ // Array.prototype.forEach.call(nodes, (item, i) => {
34
+ // removeClass(item, 'focus')
35
+ // });
36
+ // addClass(el, 'focus')
37
+ // });
38
+ // });
39
+ // });
40
+ });