sensu-dashboard 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE.txt +20 -0
- data/README.org +4 -0
- data/Rakefile +1 -0
- data/bin/sensu-dashboard +9 -0
- data/lib/sensu-dashboard/app.rb +275 -0
- data/lib/sensu-dashboard/public/css/autoSuggest.css +217 -0
- data/lib/sensu-dashboard/public/css/style.css +177 -0
- data/lib/sensu-dashboard/public/img/footer_bg.png +0 -0
- data/lib/sensu-dashboard/public/img/header_bg.png +0 -0
- data/lib/sensu-dashboard/public/img/loading_circle.gif +0 -0
- data/lib/sensu-dashboard/public/img/main_content_bg.png +0 -0
- data/lib/sensu-dashboard/public/img/megaphone_icon.png +0 -0
- data/lib/sensu-dashboard/public/img/megaphone_icon_off.png +0 -0
- data/lib/sensu-dashboard/public/js/FABridge.js +604 -0
- data/lib/sensu-dashboard/public/js/functions.js +198 -0
- data/lib/sensu-dashboard/public/js/jquery-1.5.1.min.js +16 -0
- data/lib/sensu-dashboard/public/js/jquery.autoSuggest.js +375 -0
- data/lib/sensu-dashboard/public/js/jquery.leanModal.min.js +1 -0
- data/lib/sensu-dashboard/public/js/jquery.sortElements.js +69 -0
- data/lib/sensu-dashboard/public/js/jquery.tmpl.min.js +1 -0
- data/lib/sensu-dashboard/public/js/jquery.zclip.min.js +12 -0
- data/lib/sensu-dashboard/public/js/modernizr-1.7.min.js +2 -0
- data/lib/sensu-dashboard/public/js/swfobject.js +4 -0
- data/lib/sensu-dashboard/public/js/web_socket.js +312 -0
- data/lib/sensu-dashboard/public/swf/WebSocketMain.swf +0 -0
- data/lib/sensu-dashboard/public/swf/ZeroClipboard.swf +0 -0
- data/lib/sensu-dashboard/version.rb +5 -0
- data/lib/sensu-dashboard/views/client_templates.erb +29 -0
- data/lib/sensu-dashboard/views/clients.erb +23 -0
- data/lib/sensu-dashboard/views/event_templates.erb +142 -0
- data/lib/sensu-dashboard/views/index.erb +41 -0
- data/lib/sensu-dashboard/views/layout.erb +120 -0
- data/lib/sensu-dashboard/views/sonian.sass +206 -0
- data/sensu-dashboard.gemspec +25 -0
- metadata +163 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Sonian Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.org
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/bin/sensu-dashboard
ADDED
@@ -0,0 +1,275 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'sinatra/async'
|
3
|
+
require 'em-http-request'
|
4
|
+
require 'em-websocket'
|
5
|
+
require 'sass'
|
6
|
+
require 'json'
|
7
|
+
require 'sensu/config'
|
8
|
+
|
9
|
+
options = Sensu::Config.read_arguments(ARGV)
|
10
|
+
config = Sensu::Config.new(options)
|
11
|
+
SETTINGS = config.settings
|
12
|
+
|
13
|
+
EventMachine.run do
|
14
|
+
|
15
|
+
class DashboardServer < Sinatra::Base
|
16
|
+
|
17
|
+
register Sinatra::Async
|
18
|
+
set :root, File.dirname(__FILE__)
|
19
|
+
set :static, true
|
20
|
+
set :public_folder, Proc.new { File.join(root, "public") }
|
21
|
+
|
22
|
+
api_server = 'http://' + SETTINGS['api']['host'] + ':' + SETTINGS['api']['port'].to_s
|
23
|
+
|
24
|
+
use Rack::Auth::Basic do |user, password|
|
25
|
+
user == SETTINGS['dashboard']['user'] && password == SETTINGS['dashboard']['password']
|
26
|
+
end
|
27
|
+
|
28
|
+
before do
|
29
|
+
content_type 'application/json'
|
30
|
+
end
|
31
|
+
|
32
|
+
aget '/' do
|
33
|
+
content_type 'text/html'
|
34
|
+
@js = erb :event_templates, :layout => false
|
35
|
+
body erb :index
|
36
|
+
end
|
37
|
+
|
38
|
+
aget '/clients' do
|
39
|
+
content_type 'text/html'
|
40
|
+
@js = erb :client_templates, :layout => false
|
41
|
+
body erb :clients
|
42
|
+
end
|
43
|
+
|
44
|
+
aget '/css/sonian.css' do
|
45
|
+
content_type 'text/css'
|
46
|
+
body sass :sonian
|
47
|
+
end
|
48
|
+
|
49
|
+
# api proxy
|
50
|
+
aget '/events.json' do
|
51
|
+
begin
|
52
|
+
http = EventMachine::HttpRequest.new("#{api_server}/events").get
|
53
|
+
rescue => e
|
54
|
+
puts e
|
55
|
+
status 404
|
56
|
+
body '{"error":"could not retrieve events from the sensu api"}'
|
57
|
+
end
|
58
|
+
|
59
|
+
http.errback do
|
60
|
+
status 404
|
61
|
+
body '{"error":"could not retrieve events from the sensu api"}'
|
62
|
+
end
|
63
|
+
|
64
|
+
http.callback do
|
65
|
+
status http.response_header.status
|
66
|
+
body http.response
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# api proxy
|
71
|
+
aget '/autocomplete.json' do
|
72
|
+
multi = EventMachine::MultiRequest.new
|
73
|
+
|
74
|
+
requests = [
|
75
|
+
"#{api_server}/events",
|
76
|
+
"#{api_server}/clients"
|
77
|
+
]
|
78
|
+
|
79
|
+
requests.each do |url|
|
80
|
+
multi.add EventMachine::HttpRequest.new(url).get
|
81
|
+
end
|
82
|
+
|
83
|
+
multi.callback do
|
84
|
+
events = {}
|
85
|
+
clients = []
|
86
|
+
|
87
|
+
multi.responses[:succeeded].each do |request|
|
88
|
+
body = JSON.parse(request.response)
|
89
|
+
case body
|
90
|
+
when Hash
|
91
|
+
events = body
|
92
|
+
when Array
|
93
|
+
clients = body
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
if events && clients
|
98
|
+
autocomplete = []
|
99
|
+
statuses = {:warning => [], :critical => [], :unknown => []}
|
100
|
+
subscriptions = {}
|
101
|
+
|
102
|
+
clients.each do |client|
|
103
|
+
client_name = client['name']
|
104
|
+
if events.include?(client_name)
|
105
|
+
autocomplete.push({:value => client_name, :name => client_name})
|
106
|
+
client['subscriptions'].each do |subscription|
|
107
|
+
subscriptions[subscription] ||= []
|
108
|
+
subscriptions[subscription].push(client_name)
|
109
|
+
end
|
110
|
+
events[client_name].each do |check, event|
|
111
|
+
case event["status"]
|
112
|
+
when 1
|
113
|
+
statuses[:warning].push(client_name)
|
114
|
+
when 2
|
115
|
+
statuses[:critical].push(client_name)
|
116
|
+
else
|
117
|
+
statuses[:unknown].push(client_name)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# searching by subscription
|
124
|
+
subscriptions.each do |k, v|
|
125
|
+
autocomplete.push({:value => v.join(','), :name => k})
|
126
|
+
end
|
127
|
+
|
128
|
+
# searching by status
|
129
|
+
statuses.each do |k, v|
|
130
|
+
autocomplete.push({:value => v.join(','), :name => k})
|
131
|
+
end
|
132
|
+
|
133
|
+
body autocomplete.to_json
|
134
|
+
else
|
135
|
+
status 404
|
136
|
+
body '{"error":"could not retrieve events and/or clients from the sensu api"}'
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
aget '/clients.json' do
|
142
|
+
begin
|
143
|
+
http = EventMachine::HttpRequest.new("#{api_server}/clients").get
|
144
|
+
rescue => e
|
145
|
+
puts e
|
146
|
+
status 404
|
147
|
+
body '{"error":"could not retrieve clients from the sensu api"}'
|
148
|
+
end
|
149
|
+
|
150
|
+
http.errback do
|
151
|
+
status 404
|
152
|
+
body '{"error":"could not retrieve clients from the sensu api"}'
|
153
|
+
end
|
154
|
+
|
155
|
+
http.callback do
|
156
|
+
status http.response_header.status
|
157
|
+
body http.response
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
aget '/client/:id.json' do |id|
|
162
|
+
begin
|
163
|
+
http = EventMachine::HttpRequest.new("#{api_server}/client/#{id}").get
|
164
|
+
rescue => e
|
165
|
+
puts e
|
166
|
+
status 404
|
167
|
+
body '{"error":"could not retrieve clients from the sensu api"}'
|
168
|
+
end
|
169
|
+
|
170
|
+
http.errback do
|
171
|
+
status 404
|
172
|
+
body '{"error":"could not retrieve clients from the sensu api"}'
|
173
|
+
end
|
174
|
+
|
175
|
+
http.callback do
|
176
|
+
status http.response_header.status
|
177
|
+
body http.response
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
aget '/stash/*.json' do |path|
|
182
|
+
begin
|
183
|
+
http = EventMachine::HttpRequest.new("#{api_server}/stash/#{path}").get
|
184
|
+
rescue => e
|
185
|
+
puts e
|
186
|
+
status 404
|
187
|
+
body '{"error":"could not retrieve a stash from the sensu api"}'
|
188
|
+
end
|
189
|
+
|
190
|
+
http.errback do
|
191
|
+
status 404
|
192
|
+
body '{"error":"could not retrieve a stash from the sensu api"}'
|
193
|
+
end
|
194
|
+
|
195
|
+
http.callback do
|
196
|
+
status http.response_header.status
|
197
|
+
body http.response
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
apost '/stash/*.json' do |path|
|
202
|
+
begin
|
203
|
+
http = EventMachine::HttpRequest.new("#{api_server}/stash/#{path}").post :body => request.body.read
|
204
|
+
rescue => e
|
205
|
+
puts e
|
206
|
+
status 404
|
207
|
+
body '{"error":"could not create a stash with the sensu api"}'
|
208
|
+
end
|
209
|
+
|
210
|
+
http.errback do
|
211
|
+
status 404
|
212
|
+
body '{"error":"could not create a stash with the sensu api"}'
|
213
|
+
end
|
214
|
+
|
215
|
+
http.callback do
|
216
|
+
status http.response_header.status
|
217
|
+
body http.response
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
adelete '/stash/*.json' do |path|
|
222
|
+
begin
|
223
|
+
http = EventMachine::HttpRequest.new("#{api_server}/stash/#{path}").delete
|
224
|
+
rescue => e
|
225
|
+
puts e
|
226
|
+
status 404
|
227
|
+
body '{"error":"could not delete a stash with the sensu api"}'
|
228
|
+
end
|
229
|
+
|
230
|
+
http.errback do
|
231
|
+
status 404
|
232
|
+
body '{"error":"could not delete a stash with the sensu api"}'
|
233
|
+
end
|
234
|
+
|
235
|
+
http.callback do
|
236
|
+
status http.response_header.status
|
237
|
+
body http.response
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
websocket_connections = Array.new
|
242
|
+
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 9000) do |websocket|
|
243
|
+
websocket.onopen do
|
244
|
+
websocket_connections.push websocket
|
245
|
+
puts 'client connected to websocket'
|
246
|
+
end
|
247
|
+
websocket.onclose do
|
248
|
+
websocket_connections.delete websocket
|
249
|
+
puts 'client disconnected from websocket'
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
apost '/events.json' do
|
254
|
+
unless websocket_connections.empty?
|
255
|
+
websocket_connections.each do |websocket|
|
256
|
+
websocket.send '{"update":"true"}'
|
257
|
+
end
|
258
|
+
end
|
259
|
+
body '{"success":"triggered dashboard refresh"}'
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
DashboardServer.run!({:port => SETTINGS['dashboard']['port']})
|
264
|
+
|
265
|
+
#
|
266
|
+
# Recognize exit command
|
267
|
+
#
|
268
|
+
Signal.trap("INT") do
|
269
|
+
EM.stop
|
270
|
+
end
|
271
|
+
Signal.trap("TERM") do
|
272
|
+
EM.stop
|
273
|
+
end
|
274
|
+
|
275
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
/* AutoSuggest CSS - Version 1.2 */
|
2
|
+
|
3
|
+
ul.as-selections {
|
4
|
+
list-style-type: none;
|
5
|
+
border-top: 1px solid #888;
|
6
|
+
border-bottom: 1px solid #b6b6b6;
|
7
|
+
border-left: 1px solid #aaa;
|
8
|
+
border-right: 1px solid #aaa;
|
9
|
+
padding: 4px 0 4px 4px;
|
10
|
+
margin: 0;
|
11
|
+
overflow: auto;
|
12
|
+
background-color: #fff;
|
13
|
+
box-shadow:inset 0 1px 2px #888;
|
14
|
+
-webkit-box-shadow:inset 0 1px 2px #888;
|
15
|
+
-moz-box-shadow:inset 0 1px 2px #888;
|
16
|
+
}
|
17
|
+
|
18
|
+
ul.as-selections.loading {
|
19
|
+
background-color: #eee;
|
20
|
+
}
|
21
|
+
|
22
|
+
ul.as-selections li {
|
23
|
+
float: left;
|
24
|
+
margin: 1px 4px 1px 0;
|
25
|
+
}
|
26
|
+
|
27
|
+
ul.as-selections li.as-selection-item {
|
28
|
+
color: #2b3840;
|
29
|
+
font-size: 13px;
|
30
|
+
font-family: "Lucida Grande", arial, sans-serif;
|
31
|
+
text-shadow: 0 1px 1px #fff;
|
32
|
+
background-color: #ddeefe;
|
33
|
+
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#ddeefe), to(#bfe0f1));
|
34
|
+
border: 1px solid #acc3ec;
|
35
|
+
border-top-color: #c0d9e9;
|
36
|
+
padding: 2px 7px 2px 10px;
|
37
|
+
border-radius: 12px;
|
38
|
+
-webkit-border-radius: 12px;
|
39
|
+
-moz-border-radius: 12px;
|
40
|
+
box-shadow: 0 1px 1px #e4edf2;
|
41
|
+
-webkit-box-shadow: 0 1px 1px #e4edf2;
|
42
|
+
-moz-box-shadow: 0 1px 1px #e4edf2;
|
43
|
+
}
|
44
|
+
|
45
|
+
ul.as-selections li.as-selection-item:last-child {
|
46
|
+
margin-left: 30px;
|
47
|
+
}
|
48
|
+
|
49
|
+
ul.as-selections li.as-selection-item a.as-close {
|
50
|
+
float: right;
|
51
|
+
margin: 1px 0 0 7px;
|
52
|
+
padding: 0 2px;
|
53
|
+
cursor: pointer;
|
54
|
+
color: #5491be;
|
55
|
+
font-family: "Helvetica", helvetica, arial, sans-serif;
|
56
|
+
font-size: 14px;
|
57
|
+
font-weight: bold;
|
58
|
+
text-shadow: 0 1px 1px #fff;
|
59
|
+
-webkit-transition: color .1s ease-in;
|
60
|
+
}
|
61
|
+
|
62
|
+
ul.as-selections li.as-selection-item.blur {
|
63
|
+
color: #666666;
|
64
|
+
background-color: #f4f4f4;
|
65
|
+
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#f4f4f4), to(#d5d5d5));
|
66
|
+
border-color: #bbb;
|
67
|
+
border-top-color: #ccc;
|
68
|
+
box-shadow: 0 1px 1px #e9e9e9;
|
69
|
+
-webkit-box-shadow: 0 1px 1px #e9e9e9;
|
70
|
+
-moz-box-shadow: 0 1px 1px #e9e9e9;
|
71
|
+
}
|
72
|
+
|
73
|
+
ul.as-selections li.as-selection-item.blur a.as-close {
|
74
|
+
color: #999;
|
75
|
+
}
|
76
|
+
|
77
|
+
ul.as-selections li:hover.as-selection-item {
|
78
|
+
color: #2b3840;
|
79
|
+
background-color: #bbd4f1;
|
80
|
+
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#bbd4f1), to(#a3c2e5));
|
81
|
+
border-color: #6da0e0;
|
82
|
+
border-top-color: #8bb7ed;
|
83
|
+
}
|
84
|
+
|
85
|
+
ul.as-selections li:hover.as-selection-item a.as-close {
|
86
|
+
color: #4d70b0;
|
87
|
+
}
|
88
|
+
|
89
|
+
ul.as-selections li.as-selection-item.selected {
|
90
|
+
border-color: #1f30e4;
|
91
|
+
}
|
92
|
+
|
93
|
+
ul.as-selections li.as-selection-item a:hover.as-close {
|
94
|
+
color: #1b3c65;
|
95
|
+
}
|
96
|
+
|
97
|
+
ul.as-selections li.as-selection-item a:active.as-close {
|
98
|
+
color: #4d70b0;
|
99
|
+
}
|
100
|
+
|
101
|
+
ul.as-selections li.as-original {
|
102
|
+
margin-left: 0;
|
103
|
+
}
|
104
|
+
|
105
|
+
ul.as-selections li.as-original input {
|
106
|
+
border: none;
|
107
|
+
outline: none;
|
108
|
+
font-size: 13px;
|
109
|
+
width: 120px;
|
110
|
+
height: 18px;
|
111
|
+
padding-top: 3px;
|
112
|
+
}
|
113
|
+
|
114
|
+
ul.as-list {
|
115
|
+
position: absolute;
|
116
|
+
list-style-type: none;
|
117
|
+
margin: 2px 0 0 0;
|
118
|
+
padding: 0;
|
119
|
+
font-size: 14px;
|
120
|
+
color: #000;
|
121
|
+
font-family: "Lucida Grande", arial, sans-serif;
|
122
|
+
background-color: #fff;
|
123
|
+
background-color: rgba(255,255,255,0.95);
|
124
|
+
z-index: 2;
|
125
|
+
box-shadow: 0 2px 12px #222;
|
126
|
+
-webkit-box-shadow: 0 2px 12px #222;
|
127
|
+
-moz-box-shadow: 0 2px 12px #222;
|
128
|
+
border-radius: 5px;
|
129
|
+
-webkit-border-radius: 5px;
|
130
|
+
-moz-border-radius: 5px;
|
131
|
+
}
|
132
|
+
|
133
|
+
li.as-result-item, li.as-message {
|
134
|
+
margin: 0 0 0 0;
|
135
|
+
padding: 5px 12px;
|
136
|
+
background-color: transparent;
|
137
|
+
border: 1px solid #fff;
|
138
|
+
border-bottom: 1px solid #ddd;
|
139
|
+
cursor: pointer;
|
140
|
+
border-radius: 5px;
|
141
|
+
-webkit-border-radius: 5px;
|
142
|
+
-moz-border-radius: 5px;
|
143
|
+
}
|
144
|
+
|
145
|
+
li:first-child.as-result-item {
|
146
|
+
margin: 0;
|
147
|
+
}
|
148
|
+
|
149
|
+
li.as-message {
|
150
|
+
margin: 0;
|
151
|
+
cursor: default;
|
152
|
+
}
|
153
|
+
|
154
|
+
li.as-result-item.active {
|
155
|
+
background-color: #3668d9;
|
156
|
+
background-image: -webkit-gradient(linear, 0% 0%, 0% 64%, from(rgb(110, 129, 245)), to(rgb(62, 82, 242)));
|
157
|
+
border-color: #3342e8;
|
158
|
+
color: #fff;
|
159
|
+
text-shadow: 0 1px 2px #122042;
|
160
|
+
}
|
161
|
+
|
162
|
+
li.as-result-item em {
|
163
|
+
font-style: normal;
|
164
|
+
background: #444;
|
165
|
+
padding: 0 2px;
|
166
|
+
color: #fff;
|
167
|
+
}
|
168
|
+
|
169
|
+
li.as-result-item.active em {
|
170
|
+
background: #253f7a;
|
171
|
+
color: #fff;
|
172
|
+
}
|
173
|
+
|
174
|
+
/* Webkit Hacks */
|
175
|
+
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
176
|
+
ul.as-selections {
|
177
|
+
border-top-width: 2px;
|
178
|
+
}
|
179
|
+
ul.as-selections li.as-selection-item {
|
180
|
+
padding-top: 3px;
|
181
|
+
padding-bottom: 3px;
|
182
|
+
}
|
183
|
+
ul.as-selections li.as-selection-item a.as-close {
|
184
|
+
margin-top: -1px;
|
185
|
+
}
|
186
|
+
ul.as-selections li.as-original input {
|
187
|
+
height: 19px;
|
188
|
+
}
|
189
|
+
}
|
190
|
+
|
191
|
+
/* Opera Hacks */
|
192
|
+
@media all and (-webkit-min-device-pixel-ratio:10000), not all and (-webkit-min-device-pixel-ratio:0) {
|
193
|
+
ul.as-list {
|
194
|
+
border: 1px solid #888;
|
195
|
+
}
|
196
|
+
ul.as-selections li.as-selection-item a.as-close {
|
197
|
+
margin-left: 4px;
|
198
|
+
margin-top: 0;
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
/* IE Hacks */
|
203
|
+
ul.as-list {
|
204
|
+
border: 1px solid #888\9;
|
205
|
+
}
|
206
|
+
ul.as-selections li.as-selection-item a.as-close {
|
207
|
+
margin-left: 4px\9;
|
208
|
+
margin-top: 0\9;
|
209
|
+
}
|
210
|
+
|
211
|
+
/* Firefox 3.0 Hacks */
|
212
|
+
ul.as-list, x:-moz-any-link, x:default {
|
213
|
+
border: 1px solid #888;
|
214
|
+
}
|
215
|
+
BODY:first-of-type ul.as-list, x:-moz-any-link, x:default { /* Target FF 3.5+ */
|
216
|
+
border: none;
|
217
|
+
}
|