ifttt-gdash 0.0.5 → 0.0.7
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 +5 -13
- data/lib/gdash.rb +56 -0
- data/lib/gdash/dashboard.rb +52 -0
- data/lib/gdash/monkey_patches.rb +37 -0
- data/lib/gdash/sinatra_app.rb +202 -0
- data/public/js/bootstrap-alerts.js +104 -0
- data/public/js/bootstrap-dropdown.js +50 -0
- data/public/js/bootstrap-modal.js +238 -0
- data/public/js/bootstrap-popover.js +77 -0
- data/public/js/bootstrap-scrollspy.js +105 -0
- data/public/js/bootstrap-tabs.js +62 -0
- data/public/js/bootstrap-twipsy.js +307 -0
- data/public/js/jquery-1.5.2.min.js +16 -0
- data/public/js/jquery.tablesorter.min.js +4 -0
- data/public/js/less-1.1.3.min.js +16 -0
- data/public/lib/bootstrap.less +26 -0
- data/public/lib/forms.less +465 -0
- data/public/lib/mixins.less +217 -0
- data/public/lib/patterns.less +1006 -0
- data/public/lib/reset.less +141 -0
- data/public/lib/scaffolding.less +135 -0
- data/public/lib/tables.less +170 -0
- data/public/lib/type.less +187 -0
- data/public/lib/variables.less +60 -0
- data/sample/README.md +5 -0
- data/sample/email-full-screen.png +0 -0
- data/sample/email.png +0 -0
- data/sample/email/cpu.graph +19 -0
- data/sample/email/dash.yaml +2 -0
- data/sample/email/io.graph +12 -0
- data/sample/email/load.graph +6 -0
- data/sample/email/network.graph +11 -0
- data/views/README.md +177 -0
- data/views/_interval_filter.erb +5 -0
- data/views/dashboard.erb +47 -0
- data/views/detailed_dashboard.erb +45 -0
- data/views/full_size_dashboard.erb +36 -0
- data/views/index.erb +18 -0
- data/views/layout.erb +66 -0
- metadata +55 -17
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
NjczNGNiZGYyNTY2NzZmOWJmMWZjN2FiZDVkNTdiNmI5ZThmZWM0MA==
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a905ac9bf55f0dcf3f1e033d6f994592f453578f
|
4
|
+
data.tar.gz: 668819d176f3a2e3ac0a0971d73bc7bfa1839ad3
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
OGFiNzczOTUxOGMxNzI5MDM0YzljMjM5YzJiZWY3NTI5YWJhMzQzMTgwZmE4
|
11
|
-
MWQ3NTg2MzViNDViNDMzMDIwZGY0NTJmMTExMGQ5YjBkOWI2NjE=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
OGIyZGRkYjc2MDNmMDViYjkzYjZhMjY5ZjQ1MjRhZDA0MGYxZmVhMzJhOThk
|
14
|
-
ZGI0YjU3YzBiOTFhZmY4MTk2ZjJlYjQ1NzI2OGIwZmNlNDU0Y2Y2NzQxZWVl
|
15
|
-
MTYzNmIwZmRlOWIzMjgwNTQwNmYxNWQxNjU3NjQ0MGRkYmZkNTM=
|
6
|
+
metadata.gz: b3924408b437fcadd12d50a1900266fb002e18177285b9690162dc2bf2dd55a257f315e1a73764411fa435d7ae0625d458ba82226c0f4f67af1f277a698cd478
|
7
|
+
data.tar.gz: 21a0514bb3ccfa2c553cf73fa1f9dcd67af640935865b6fec76bde7e5734f856a237fa1266b1b6b1ffc0064e4170f3df1e01d89bf205bae9e547f49a3820d73d
|
data/lib/gdash.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'yaml'
|
4
|
+
require 'erb'
|
5
|
+
require 'redcarpet'
|
6
|
+
|
7
|
+
class GDash
|
8
|
+
require 'gdash/dashboard'
|
9
|
+
require 'gdash/monkey_patches'
|
10
|
+
require 'gdash/sinatra_app'
|
11
|
+
require 'graphite_graph'
|
12
|
+
|
13
|
+
attr_reader :graphite_base, :graphite_render, :dash_templates, :height, :width, :from, :until
|
14
|
+
|
15
|
+
def initialize(graphite_base, render_url, dash_templates, options={})
|
16
|
+
@graphite_base = graphite_base
|
17
|
+
@graphite_render = [@graphite_base, "/render/"].join
|
18
|
+
@dash_templates = dash_templates
|
19
|
+
@height = options.delete(:height)
|
20
|
+
@width = options.delete(:width)
|
21
|
+
@from = options.delete(:from)
|
22
|
+
@until = options.delete(:until)
|
23
|
+
|
24
|
+
raise "Dashboard templates directory #{@dash_templates} does not exist" unless File.directory?(@dash_templates)
|
25
|
+
end
|
26
|
+
|
27
|
+
def dashboard(name, options={})
|
28
|
+
options[:width] ||= @width
|
29
|
+
options[:height] ||= @height
|
30
|
+
options[:from] ||= @from
|
31
|
+
options[:until] ||= @until
|
32
|
+
|
33
|
+
Dashboard.new(name, dash_templates, options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def list
|
37
|
+
dashboards.map {|dash| dash[:link]}
|
38
|
+
end
|
39
|
+
|
40
|
+
def dashboards
|
41
|
+
dashboards = []
|
42
|
+
|
43
|
+
Dir.entries(dash_templates).each do |dash|
|
44
|
+
begin
|
45
|
+
yaml_file = File.join(dash_templates, dash, "dash.yaml")
|
46
|
+
if File.exist?(yaml_file)
|
47
|
+
dashboards << YAML.load_file(yaml_file).merge({:link => dash})
|
48
|
+
end
|
49
|
+
rescue Exception => e
|
50
|
+
p e
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
dashboards.sort_by{|d| d[:name].to_s}
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
class GDash
|
2
|
+
class Dashboard
|
3
|
+
attr_accessor :properties
|
4
|
+
|
5
|
+
def initialize(short_name, dir, options={})
|
6
|
+
raise "Cannot find dashboard directory #{dir}" unless File.directory?(dir)
|
7
|
+
|
8
|
+
@properties = {:graph_width => nil,
|
9
|
+
:graph_height => nil,
|
10
|
+
:graph_from => nil,
|
11
|
+
:graph_until => nil}
|
12
|
+
|
13
|
+
@properties[:short_name] = short_name
|
14
|
+
@properties[:directory] = File.join(dir, short_name)
|
15
|
+
@properties[:yaml] = File.join(dir, short_name, "dash.yaml")
|
16
|
+
|
17
|
+
raise "Cannot find YAML file #{yaml}" unless File.exist?(yaml)
|
18
|
+
|
19
|
+
@properties.merge!(YAML.load_file(yaml))
|
20
|
+
|
21
|
+
# Properties defined in dashboard config file are overridden when given on initialization
|
22
|
+
@properties[:graph_width] = options.delete(:width) || graph_width
|
23
|
+
@properties[:graph_height] = options.delete(:height) || graph_height
|
24
|
+
@properties[:graph_from] = options.delete(:from) || graph_from
|
25
|
+
@properties[:graph_until] = options.delete(:until) || graph_until
|
26
|
+
@properties[:options] = options
|
27
|
+
end
|
28
|
+
|
29
|
+
def graphs(options={})
|
30
|
+
options[:width] ||= graph_width
|
31
|
+
options[:height] ||= graph_height
|
32
|
+
options[:from] ||= graph_from
|
33
|
+
options[:until] ||= graph_until
|
34
|
+
|
35
|
+
graphs = Dir.entries(directory).select{|f| f.match(/\.graph$/)}
|
36
|
+
|
37
|
+
overrides = options.reject { |k,v| v.nil? }
|
38
|
+
|
39
|
+
graphs.sort.map do |graph|
|
40
|
+
{:name => File.basename(graph, ".graph"), :graphite => GraphiteGraph.new(File.join(directory, graph), overrides)}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def method_missing(method, *args)
|
45
|
+
if properties.include?(method)
|
46
|
+
properties[method]
|
47
|
+
else
|
48
|
+
super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Array
|
2
|
+
def in_groups_of(chunk_size, padded_with=nil)
|
3
|
+
if chunk_size <= 1
|
4
|
+
if block_given?
|
5
|
+
self.each{|a| yield([a])}
|
6
|
+
else
|
7
|
+
self
|
8
|
+
end
|
9
|
+
else
|
10
|
+
arr = self.clone
|
11
|
+
|
12
|
+
# how many to add
|
13
|
+
padding = chunk_size - (arr.size % chunk_size)
|
14
|
+
padding = 0 if padding == chunk_size
|
15
|
+
|
16
|
+
# pad at the end
|
17
|
+
arr.concat([padded_with] * padding)
|
18
|
+
|
19
|
+
# how many chunks we'll make
|
20
|
+
count = arr.size / chunk_size
|
21
|
+
|
22
|
+
# make that many arrays
|
23
|
+
result = []
|
24
|
+
count.times {|s| result << arr[s * chunk_size, chunk_size]}
|
25
|
+
|
26
|
+
if block_given?
|
27
|
+
result.each{|a| yield(a)}
|
28
|
+
else
|
29
|
+
result
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class GraphiteGraph
|
36
|
+
attr_accessor :properties, :file
|
37
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
class GDash
|
2
|
+
class SinatraApp < ::Sinatra::Base
|
3
|
+
def initialize(graphite_base, graph_templates, options = {})
|
4
|
+
# where the whisper data is
|
5
|
+
@whisper_dir = options.delete(:whisper_dir) || "/var/lib/carbon/whisper"
|
6
|
+
|
7
|
+
# where graphite lives
|
8
|
+
@graphite_base = graphite_base
|
9
|
+
|
10
|
+
# where the graphite renderer is
|
11
|
+
@graphite_render = [@graphite_base, "/render/"].join
|
12
|
+
|
13
|
+
# where to find graph, dash etc templates
|
14
|
+
@graph_templates = graph_templates
|
15
|
+
|
16
|
+
# the dash site might have a prefix for its css etc
|
17
|
+
@prefix = options.delete(:prefix) || ""
|
18
|
+
|
19
|
+
# the page refresh rate
|
20
|
+
@refresh_rate = options.delete(:refresh_rate) || 60
|
21
|
+
|
22
|
+
# how many columns of graphs do you want on a page
|
23
|
+
@graph_columns = options.delete(:graph_columns) || 2
|
24
|
+
|
25
|
+
# how wide each graph should be
|
26
|
+
@graph_width = options.delete(:graph_width)
|
27
|
+
|
28
|
+
# how hight each graph sould be
|
29
|
+
@graph_height = options.delete(:graph_height)
|
30
|
+
|
31
|
+
# Dashboard title
|
32
|
+
@dash_title = options.delete(:title) || "Graphite Dashboard"
|
33
|
+
|
34
|
+
# Time filters in interface
|
35
|
+
@interval_filters = options.delete(:interval_filters) || Array.new
|
36
|
+
|
37
|
+
@intervals = options.delete(:intervals) || []
|
38
|
+
|
39
|
+
@top_level = Hash.new
|
40
|
+
Dir.entries(@graph_templates).each do |category|
|
41
|
+
if File.directory?("#{@graph_templates}/#{category}")
|
42
|
+
unless ("#{category}" =~ /^\./ )
|
43
|
+
@top_level["#{category}"] = GDash.new(@graphite_base, "/render/", File.join(@graph_templates, "/#{category}"), {:width => @graph_width, :height => @graph_height})
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
super()
|
49
|
+
end
|
50
|
+
|
51
|
+
set :static, true
|
52
|
+
set :views, File.join(File.expand_path(File.dirname(__FILE__)), "../..", "views")
|
53
|
+
if Sinatra.const_defined?("VERSION") && Gem::Version.new(Sinatra::VERSION) >= Gem::Version.new("1.3.0")
|
54
|
+
set :public_folder, File.join(File.expand_path(File.dirname(__FILE__)), "../..", "public")
|
55
|
+
else
|
56
|
+
set :public, File.join(File.expand_path(File.dirname(__FILE__)), "../..", "public")
|
57
|
+
end
|
58
|
+
|
59
|
+
get '/' do
|
60
|
+
if @top_level.empty?
|
61
|
+
@error = "No dashboards found in the templates directory"
|
62
|
+
end
|
63
|
+
|
64
|
+
erb :index
|
65
|
+
end
|
66
|
+
|
67
|
+
get '/:category/:dash/details/:name' do
|
68
|
+
options = query_params.except(:from, :until)
|
69
|
+
if @top_level["#{params[:category]}"].list.include?(params[:dash])
|
70
|
+
@dashboard = @top_level[@params[:category]].dashboard(params[:dash], options)
|
71
|
+
else
|
72
|
+
@error = "No dashboard called #{params[:dash]} found in #{params[:category]}/#{@top_level[params[:category]].list.join ','}."
|
73
|
+
end
|
74
|
+
|
75
|
+
if @intervals.empty?
|
76
|
+
@error = "No intervals defined in configuration"
|
77
|
+
end
|
78
|
+
|
79
|
+
if main_graph = @dashboard.graphs[params[:name].to_i][:graphite]
|
80
|
+
@graphs = @intervals.map do |e|
|
81
|
+
new_props = {:from => e[0], :title => "#{main_graph.properties[:title]} - #{e[1]}"}
|
82
|
+
new_props = main_graph.properties.merge new_props
|
83
|
+
GraphiteGraph.new(main_graph.file, new_props)
|
84
|
+
end
|
85
|
+
else
|
86
|
+
@error = "No such graph available"
|
87
|
+
end
|
88
|
+
|
89
|
+
erb :detailed_dashboard
|
90
|
+
end
|
91
|
+
|
92
|
+
get '/:category/:dash/full/?*' do
|
93
|
+
options = {}
|
94
|
+
params["splat"] = params["splat"].first.split("/")
|
95
|
+
|
96
|
+
params["columns"] = params["splat"][0].to_i || @graph_columns
|
97
|
+
|
98
|
+
if params["splat"].size == 3
|
99
|
+
options[:width] = params["splat"][1].to_i
|
100
|
+
options[:height] = params["splat"][2].to_i
|
101
|
+
else
|
102
|
+
options[:width] = @graph_width
|
103
|
+
options[:height] = @graph_height
|
104
|
+
end
|
105
|
+
|
106
|
+
options.merge!(query_params)
|
107
|
+
|
108
|
+
if @top_level["#{params[:category]}"].list.include?(params[:dash])
|
109
|
+
@dashboard = @top_level[@params[:category]].dashboard(params[:dash], options)
|
110
|
+
else
|
111
|
+
@error = "No dashboard called #{params[:dash]} found in #{params[:category]}/#{@top_level[params[:category]].list.join ','}"
|
112
|
+
end
|
113
|
+
|
114
|
+
erb :full_size_dashboard, :layout => false
|
115
|
+
end
|
116
|
+
|
117
|
+
get '/:category/:dash/?*' do
|
118
|
+
options = {}
|
119
|
+
params["splat"] = params["splat"].first.split("/")
|
120
|
+
|
121
|
+
case params["splat"][0]
|
122
|
+
when 'time'
|
123
|
+
options[:from] = params["splat"][1] || "-1hour"
|
124
|
+
options[:until] = params["splat"][2] || "now"
|
125
|
+
end
|
126
|
+
|
127
|
+
options.merge!(query_params)
|
128
|
+
|
129
|
+
if @top_level["#{params[:category]}"].list.include?(params[:dash])
|
130
|
+
@dashboard = @top_level[@params[:category]].dashboard(params[:dash], options)
|
131
|
+
else
|
132
|
+
@error = "No dashboard called #{params[:dash]} found in #{params[:category]}/#{@top_level[params[:category]].list.join ','}."
|
133
|
+
end
|
134
|
+
|
135
|
+
erb :dashboard
|
136
|
+
end
|
137
|
+
|
138
|
+
get '/docs/' do
|
139
|
+
markdown :README, :layout_engine => :erb
|
140
|
+
end
|
141
|
+
|
142
|
+
helpers do
|
143
|
+
include Rack::Utils
|
144
|
+
|
145
|
+
alias_method :h, :escape_html
|
146
|
+
|
147
|
+
def current_top_level
|
148
|
+
@top_level[@params[:category]]
|
149
|
+
end
|
150
|
+
|
151
|
+
def query_params
|
152
|
+
hash = {}
|
153
|
+
protected_keys = [:category, :dash, :splat]
|
154
|
+
|
155
|
+
params.each do |k, v|
|
156
|
+
hash[k.to_sym] = v unless protected_keys.include?(k.to_sym)
|
157
|
+
end
|
158
|
+
|
159
|
+
hash
|
160
|
+
end
|
161
|
+
|
162
|
+
def uri_to_dashboard(dash, options = {})
|
163
|
+
category = options[:category] || params[:category]
|
164
|
+
if options[:from]
|
165
|
+
uri = URI([@prefix, category, dash, 'time', h(options[:from]), h(options[:to])].join('/'))
|
166
|
+
else
|
167
|
+
uri = URI([@prefix, category, dash].join('/'))
|
168
|
+
end
|
169
|
+
query = request.query_string.empty? ? {} : Rack::Utils.parse_nested_query(request.query_string)
|
170
|
+
query.merge!((options[:query] || {}).stringify_keys)
|
171
|
+
uri.query = query.to_query unless query.empty?
|
172
|
+
uri.to_s
|
173
|
+
end
|
174
|
+
|
175
|
+
def uri_to_interval(options)
|
176
|
+
uri = URI([@prefix, params[:category], params[:dash], 'time', h(options[:from]), h(options[:to])].join('/'))
|
177
|
+
uri.query = request.query_string unless request.query_string.empty?
|
178
|
+
uri.to_s
|
179
|
+
end
|
180
|
+
|
181
|
+
def link_to_interval(options)
|
182
|
+
"<a href=\"#{ uri_to_interval(options) }\">#{ h(options[:label]) }</a>"
|
183
|
+
end
|
184
|
+
|
185
|
+
def uri_to_details(index)
|
186
|
+
uri = URI([@prefix, params[:category], @params[:dash], 'details', index].join('/'))
|
187
|
+
uri.query = request.query_string unless request.query_string.empty?
|
188
|
+
uri.to_s
|
189
|
+
end
|
190
|
+
|
191
|
+
# To be overriden by base classes, should they so desire.
|
192
|
+
# @dashboard holds the dashboard
|
193
|
+
def dashboard_preheader; ''; end
|
194
|
+
def dashboard_header; ''; end
|
195
|
+
def graph_caption(graph); ''; end
|
196
|
+
end
|
197
|
+
|
198
|
+
before do
|
199
|
+
content_type :html, 'charset' => 'utf-8'
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
/* ==========================================================
|
2
|
+
* bootstrap-alerts.js v1.3.0
|
3
|
+
* http://twitter.github.com/bootstrap/javascript.html#alerts
|
4
|
+
* ==========================================================
|
5
|
+
* Copyright 2011 Twitter, Inc.
|
6
|
+
*
|
7
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
* you may not use this file except in compliance with the License.
|
9
|
+
* You may obtain a copy of the License at
|
10
|
+
*
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
*
|
13
|
+
* Unless required by applicable law or agreed to in writing, software
|
14
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
* See the License for the specific language governing permissions and
|
17
|
+
* limitations under the License.
|
18
|
+
* ========================================================== */
|
19
|
+
|
20
|
+
|
21
|
+
!function( $ ){
|
22
|
+
|
23
|
+
/* CSS TRANSITION SUPPORT (https://gist.github.com/373874)
|
24
|
+
* ======================================================= */
|
25
|
+
|
26
|
+
var transitionEnd
|
27
|
+
|
28
|
+
$(document).ready(function () {
|
29
|
+
|
30
|
+
$.support.transition = (function () {
|
31
|
+
var thisBody = document.body || document.documentElement
|
32
|
+
, thisStyle = thisBody.style
|
33
|
+
, support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined
|
34
|
+
return support
|
35
|
+
})()
|
36
|
+
|
37
|
+
// set CSS transition event type
|
38
|
+
if ( $.support.transition ) {
|
39
|
+
transitionEnd = "TransitionEnd"
|
40
|
+
if ( $.browser.webkit ) {
|
41
|
+
transitionEnd = "webkitTransitionEnd"
|
42
|
+
} else if ( $.browser.mozilla ) {
|
43
|
+
transitionEnd = "transitionend"
|
44
|
+
} else if ( $.browser.opera ) {
|
45
|
+
transitionEnd = "oTransitionEnd"
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
})
|
50
|
+
|
51
|
+
/* ALERT CLASS DEFINITION
|
52
|
+
* ====================== */
|
53
|
+
|
54
|
+
var Alert = function ( content, selector ) {
|
55
|
+
this.$element = $(content)
|
56
|
+
.delegate(selector || '.close', 'click', this.close)
|
57
|
+
}
|
58
|
+
|
59
|
+
Alert.prototype = {
|
60
|
+
|
61
|
+
close: function (e) {
|
62
|
+
var $element = $(this).parent('.alert-message')
|
63
|
+
|
64
|
+
e && e.preventDefault()
|
65
|
+
$element.removeClass('in')
|
66
|
+
|
67
|
+
function removeElement () {
|
68
|
+
$element.remove()
|
69
|
+
}
|
70
|
+
|
71
|
+
$.support.transition && $element.hasClass('fade') ?
|
72
|
+
$element.bind(transitionEnd, removeElement) :
|
73
|
+
removeElement()
|
74
|
+
}
|
75
|
+
|
76
|
+
}
|
77
|
+
|
78
|
+
|
79
|
+
/* ALERT PLUGIN DEFINITION
|
80
|
+
* ======================= */
|
81
|
+
|
82
|
+
$.fn.alert = function ( options ) {
|
83
|
+
|
84
|
+
if ( options === true ) {
|
85
|
+
return this.data('alert')
|
86
|
+
}
|
87
|
+
|
88
|
+
return this.each(function () {
|
89
|
+
var $this = $(this)
|
90
|
+
|
91
|
+
if ( typeof options == 'string' ) {
|
92
|
+
return $this.data('alert')[options]()
|
93
|
+
}
|
94
|
+
|
95
|
+
$(this).data('alert', new Alert( this ))
|
96
|
+
|
97
|
+
})
|
98
|
+
}
|
99
|
+
|
100
|
+
$(document).ready(function () {
|
101
|
+
new Alert($('body'), '.alert-message[data-alert] .close')
|
102
|
+
})
|
103
|
+
|
104
|
+
}( window.jQuery || window.ender );
|