kthxbye 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +33 -0
- data/DESIGN.textile +81 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +42 -0
- data/LICENSE +20 -0
- data/README.textile +91 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/config.ru +7 -0
- data/lib/kthxbye.rb +151 -0
- data/lib/kthxbye/config.rb +35 -0
- data/lib/kthxbye/exceptions.rb +4 -0
- data/lib/kthxbye/failure.rb +62 -0
- data/lib/kthxbye/helper.rb +42 -0
- data/lib/kthxbye/job.rb +127 -0
- data/lib/kthxbye/version.rb +5 -0
- data/lib/kthxbye/web_interface.rb +117 -0
- data/lib/kthxbye/web_interface/public/application.js +16 -0
- data/lib/kthxbye/web_interface/public/awesome-buttons.css +108 -0
- data/lib/kthxbye/web_interface/public/jquery.js +154 -0
- data/lib/kthxbye/web_interface/public/style.css +128 -0
- data/lib/kthxbye/web_interface/views/error.haml +5 -0
- data/lib/kthxbye/web_interface/views/failed.haml +26 -0
- data/lib/kthxbye/web_interface/views/hash.haml +6 -0
- data/lib/kthxbye/web_interface/views/layout.haml +33 -0
- data/lib/kthxbye/web_interface/views/overview.haml +2 -0
- data/lib/kthxbye/web_interface/views/queues.haml +31 -0
- data/lib/kthxbye/web_interface/views/set.haml +4 -0
- data/lib/kthxbye/web_interface/views/stats.haml +32 -0
- data/lib/kthxbye/web_interface/views/view_backtrace.haml +8 -0
- data/lib/kthxbye/web_interface/views/workers.haml +24 -0
- data/lib/kthxbye/web_interface/views/working.haml +19 -0
- data/lib/kthxbye/worker.rb +221 -0
- data/test/helper.rb +18 -0
- data/test/redis-test.conf +115 -0
- data/test/test_failure.rb +51 -0
- data/test/test_helper.rb +86 -0
- data/test/test_kthxbye.rb +213 -0
- data/test/test_worker.rb +148 -0
- metadata +364 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
module Kthxbye
|
2
|
+
module Config
|
3
|
+
|
4
|
+
# default options for Kthxbye
|
5
|
+
#
|
6
|
+
# redis_server = the ip to connect to by defaut
|
7
|
+
#
|
8
|
+
# redis_port = default redis port
|
9
|
+
#
|
10
|
+
# attempts = default number of attempts on a failing job
|
11
|
+
# before moving to the failed job store
|
12
|
+
#
|
13
|
+
# vervose = more output
|
14
|
+
#
|
15
|
+
DEFAULT = {:redis_server => '127.0.0.1',
|
16
|
+
:redis_port => 9876,
|
17
|
+
:attempts => 1,
|
18
|
+
:verbose => false}.freeze
|
19
|
+
|
20
|
+
# configures any other args input by the user.
|
21
|
+
# can pull from a config.yaml file as well.
|
22
|
+
#
|
23
|
+
def self.setup( args=nil )
|
24
|
+
@options = DEFAULT.dup
|
25
|
+
@options.merge!( YAML.load('config.yaml') ) if File.exist?( 'config.yaml' )
|
26
|
+
@options.merge!( args ) if args
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.options
|
30
|
+
return @options if @options
|
31
|
+
Config.setup
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Kthxbye
|
2
|
+
module Failure
|
3
|
+
include Helper
|
4
|
+
extend Helper
|
5
|
+
|
6
|
+
def self.all
|
7
|
+
redis.hvals( :failed ).sort.map{|x| decode( x )}
|
8
|
+
end
|
9
|
+
|
10
|
+
# returns a specified failed job data
|
11
|
+
def self.find(id)
|
12
|
+
decode( redis.hget( :failed, id ) )
|
13
|
+
end
|
14
|
+
|
15
|
+
# gets count of all errors
|
16
|
+
def self.count
|
17
|
+
redis.hkeys( :failed ).size
|
18
|
+
end
|
19
|
+
|
20
|
+
# gets count of all errors of a specific type
|
21
|
+
def self.count_type(type)
|
22
|
+
vals = redis.hvals( :failed )
|
23
|
+
vals.each {|x| o = decode(x); vals.delete x if o['type'] !~ /#{type.to_s}/}
|
24
|
+
vals.size
|
25
|
+
end
|
26
|
+
|
27
|
+
# creates a Failure object.
|
28
|
+
def self.create(job, exception)
|
29
|
+
failed_attempts = (Failure.find(job.id)['attempts'].to_i + 1) if redis.hexists( :failed, job.id )
|
30
|
+
|
31
|
+
error = {
|
32
|
+
:type => exception.class.to_s,
|
33
|
+
:error => exception.to_s,
|
34
|
+
:job => job.id,
|
35
|
+
:queue => job.queue,
|
36
|
+
:time => Time.now,
|
37
|
+
:backtrace => Array( exception.backtrace ),
|
38
|
+
:attempts => (failed_attempts || 1)
|
39
|
+
}
|
40
|
+
|
41
|
+
redis.hset( :failed, job.id, encode( error ) )
|
42
|
+
|
43
|
+
job.dequeue
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.rerun(id)
|
47
|
+
failure = Failure.find(id)
|
48
|
+
Job.find(id, failure['queue']).rerun
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.fails_for_job(id)
|
52
|
+
failure = decode( redis.hget( :failed, id ) )
|
53
|
+
return failure ? failure['attempts'] : 0
|
54
|
+
end
|
55
|
+
|
56
|
+
# the only method allowed to clear exceptions out of the exception store
|
57
|
+
def self.clear_exception(id)
|
58
|
+
redis.hdel( :failed, id )
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Kthxbye
|
2
|
+
module Helper
|
3
|
+
|
4
|
+
def redis
|
5
|
+
Kthxbye.redis
|
6
|
+
end
|
7
|
+
|
8
|
+
def log(msg)
|
9
|
+
if Kthxbye::Config.options[:verbose]
|
10
|
+
puts "!! #{msg} - #{Time.now.strftime("%I:%M%p")}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# encode/decode code taken and modified from Resque
|
16
|
+
# (http://github.com/defunkt/resque/blob/master/lib/resque/helpers.rb)
|
17
|
+
#
|
18
|
+
def encode( data )
|
19
|
+
if defined? Yajl
|
20
|
+
Yajl::Encoder.encode(data)
|
21
|
+
else
|
22
|
+
data.to_json
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def decode( data )
|
27
|
+
return unless data
|
28
|
+
|
29
|
+
if defined? Yajl
|
30
|
+
begin
|
31
|
+
Yajl::Parser.parse( data, :check_utf8 => false )
|
32
|
+
rescue Yajl::ParseError
|
33
|
+
end
|
34
|
+
else
|
35
|
+
begin
|
36
|
+
JSON.parse( data )
|
37
|
+
rescue JSON::ParseError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/kthxbye/job.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
module Kthxbye
|
2
|
+
class Job
|
3
|
+
include Helper
|
4
|
+
extend Helper
|
5
|
+
|
6
|
+
attr_accessor :id, :queue, :data, :worker
|
7
|
+
attr_reader :failed_attempts
|
8
|
+
|
9
|
+
def self.add_to_queue(queue, id)
|
10
|
+
redis.rpush( "queue:#{queue}", id )
|
11
|
+
end
|
12
|
+
|
13
|
+
# insert a job into the queue
|
14
|
+
def self.create(queue, klass, *args)
|
15
|
+
raise "Need a queue to store job in" if queue.to_s.empty?
|
16
|
+
raise "No class to reference job type by" if klass.nil?
|
17
|
+
|
18
|
+
redis.incr :uniq_id
|
19
|
+
id = redis.get :uniq_id
|
20
|
+
|
21
|
+
Job.add_to_queue( queue, id )
|
22
|
+
Kthxbye.register_queue( queue )
|
23
|
+
|
24
|
+
redis.hset( "data-store:#{queue}", id, encode( {:klass => klass, :payload => args} ) )
|
25
|
+
log "Created job in queue #{queue} with an unique key of #{id}"
|
26
|
+
|
27
|
+
return id.to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.find(id, queue)
|
31
|
+
data = decode( redis.hget( "data-store:#{queue}", id ) )
|
32
|
+
data ? Job.new(id, queue, data) : nil
|
33
|
+
end
|
34
|
+
|
35
|
+
# removes all existence of this job and its data
|
36
|
+
# returns the last known status of the job
|
37
|
+
def self.destroy(id, queue)
|
38
|
+
ret = Job.find(id, queue).status
|
39
|
+
|
40
|
+
# remove the element from the active queue
|
41
|
+
redis.lrem("queue:#{queue}", 0, id)
|
42
|
+
# be sure we also remove it from the inactive queue
|
43
|
+
redis.srem("queue:#{queue}:inactive", id)
|
44
|
+
# remove the job's data as well
|
45
|
+
redis.hdel("data-store:#{queue}", id)
|
46
|
+
redis.hdel("result-store:#{queue}", id)
|
47
|
+
redis.hdel( :faulure, id )
|
48
|
+
|
49
|
+
return ret
|
50
|
+
end
|
51
|
+
|
52
|
+
# instantiates a job for the worker to run
|
53
|
+
def initialize(id, queue, data)
|
54
|
+
@id = id.to_i
|
55
|
+
@queue = queue
|
56
|
+
@data = data
|
57
|
+
@failed_attempts = Failure.fails_for_job(@id) # local tracking only, for rerun purposes
|
58
|
+
end
|
59
|
+
|
60
|
+
# simply requeues a job
|
61
|
+
def rerun
|
62
|
+
Job.add_to_queue( @queue, @id )
|
63
|
+
end
|
64
|
+
|
65
|
+
def status
|
66
|
+
if result
|
67
|
+
:succeeded
|
68
|
+
elsif Failure.find(@id)
|
69
|
+
:failed
|
70
|
+
elsif active?
|
71
|
+
:active
|
72
|
+
else
|
73
|
+
:inactive
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def result
|
78
|
+
decode( redis.hget("result-store:#{@queue}", @id) )
|
79
|
+
end
|
80
|
+
|
81
|
+
# simply removes this job from the active queue and places it
|
82
|
+
# on the inactive list.
|
83
|
+
# does not remove job payload
|
84
|
+
def dequeue
|
85
|
+
redis.lrem("queue:#{@queue}", 0, @id)
|
86
|
+
inactive
|
87
|
+
end
|
88
|
+
|
89
|
+
# does the heavy lifting of running a job
|
90
|
+
def perform
|
91
|
+
begin
|
92
|
+
@klass = Object.const_get(@data['klass'])
|
93
|
+
@payload = @data['payload']
|
94
|
+
|
95
|
+
result = @klass.send(:perform, *@payload)
|
96
|
+
redis.hset( "result-store:#{@queue}", @id, encode( result ) )
|
97
|
+
return result
|
98
|
+
rescue Exception => ex
|
99
|
+
@failed_attempts += 1
|
100
|
+
log "Error occured: #{ex.message}. Try: #{@failed_attempts}/#{Kthxbye::Config.options[:attempts]}"
|
101
|
+
return Kthxbye::Failure.create( self, ex ) if @failed_attempts >= Kthxbye::Config.options[:attempts]
|
102
|
+
perform
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# will allow us to track when this job is being worked
|
107
|
+
def active
|
108
|
+
redis.srem("jobs:inactive", @id)
|
109
|
+
end
|
110
|
+
|
111
|
+
def active?
|
112
|
+
!redis.sismember("jobs:inactive", @id)
|
113
|
+
end
|
114
|
+
|
115
|
+
def inactive
|
116
|
+
redis.sadd("jobs:inactive", @id)
|
117
|
+
end
|
118
|
+
|
119
|
+
def ==(obj)
|
120
|
+
return false if obj.nil?
|
121
|
+
@data == obj.data &&
|
122
|
+
@id == obj.id &&
|
123
|
+
@queue == obj.queue
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'haml'
|
3
|
+
require 'kthxbye'
|
4
|
+
|
5
|
+
module Kthxbye
|
6
|
+
class WebInterface < Sinatra::Base
|
7
|
+
base = File.dirname( File.expand_path(__FILE__) )
|
8
|
+
|
9
|
+
set :views, "#{base}/web_interface/views"
|
10
|
+
set :public, "#{base}/web_interface/public"
|
11
|
+
set :static, true
|
12
|
+
set :environment, :development
|
13
|
+
|
14
|
+
helpers do
|
15
|
+
include Rack::Utils
|
16
|
+
|
17
|
+
def toggle_poll
|
18
|
+
if @active
|
19
|
+
text = "Last update: #{Time.now.strftime("%H:%M:%S")}"
|
20
|
+
else
|
21
|
+
text = "<a rel='poll_link' class='awesome button small' href='#{url(request.path_info)}.poll'>Live Poll</a>"
|
22
|
+
end
|
23
|
+
"<div id='poll_status'>#{text}</div>"
|
24
|
+
end
|
25
|
+
|
26
|
+
def url(*path_parts)
|
27
|
+
[ path_prefix, path_parts ].join("/").squeeze('/')
|
28
|
+
end
|
29
|
+
|
30
|
+
def path_prefix
|
31
|
+
request.env['SCRIPT_NAME']
|
32
|
+
end
|
33
|
+
|
34
|
+
def show(page, layout=true)
|
35
|
+
begin
|
36
|
+
haml page.to_sym, {:layout => layout}, :kthxbye => Kthxbye
|
37
|
+
rescue Errno::ECONNREFUSED
|
38
|
+
haml :error, {:layout => false}, :error => "Can't connect to Redis at #{Kthxbye::Config.options[:redis_server]}:#{Kthxbye::Config.options[:redis_port]}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def partial(template, local_vars = {})
|
43
|
+
haml template.to_sym, {:layout => false}, local_vars
|
44
|
+
end
|
45
|
+
|
46
|
+
def current_url
|
47
|
+
url request.path_info.split("/")
|
48
|
+
end
|
49
|
+
|
50
|
+
def mark_current(page)
|
51
|
+
'current' if current_url[1..-1] == page.split(" ").first
|
52
|
+
end
|
53
|
+
|
54
|
+
def tab(name)
|
55
|
+
name.to_s.downcase!
|
56
|
+
path = url(name)
|
57
|
+
"<a href=#{path}><span class='tab #{mark_current(name)}'>#{name}</span></a>"
|
58
|
+
end
|
59
|
+
|
60
|
+
def tabs
|
61
|
+
@tabs ||= ["Overview", "Workers (#{Kthxbye.workers.size})", "Failed (#{Kthxbye::Failure.all.size})", "Queues (#{Kthxbye.queues.size})", "Stats"]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# default route
|
66
|
+
get "/?" do
|
67
|
+
redirect url(:overview)
|
68
|
+
end
|
69
|
+
|
70
|
+
%w(overview workers failed queues stats).each do |page|
|
71
|
+
get "/#{page}" do
|
72
|
+
show page
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
get "/stats/:id" do
|
77
|
+
show :stats
|
78
|
+
end
|
79
|
+
|
80
|
+
get "/stats/keys/:key" do
|
81
|
+
show :stats
|
82
|
+
end
|
83
|
+
|
84
|
+
%w(overview workers).each do |page|
|
85
|
+
get "/#{page}.poll" do
|
86
|
+
@active = true
|
87
|
+
content_type "text/plain"
|
88
|
+
show(page.to_sym, false)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
get "/view_backtrace" do
|
93
|
+
@backtrace = Kthxbye::Failure.find(params[:id])['backtrace']
|
94
|
+
haml :view_backtrace, {:layout => :layout}
|
95
|
+
end
|
96
|
+
|
97
|
+
post "/queues/:q/remove" do
|
98
|
+
Kthxbye.unregister_queue(params[:q])
|
99
|
+
redirect url(:overview)
|
100
|
+
end
|
101
|
+
|
102
|
+
post "/failed/:job/rerun" do
|
103
|
+
Kthxbye::Failure.rerun(params[:job])
|
104
|
+
redirect url(:failed)
|
105
|
+
end
|
106
|
+
|
107
|
+
post "/failed/:job/clear" do
|
108
|
+
job = Kthxbye::Failure.clear_exception(params[:job])
|
109
|
+
redirect url(:failed)
|
110
|
+
end
|
111
|
+
|
112
|
+
post "/toggle_polling" do
|
113
|
+
redirect url(:overview)
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
$(function() {
|
2
|
+
var poll_interval = 2
|
3
|
+
|
4
|
+
$('a[rel="poll_link"]').click(function(e) {
|
5
|
+
e.preventDefault()
|
6
|
+
var action = this.href
|
7
|
+
|
8
|
+
$('div#poll_status').html("Starting...")
|
9
|
+
|
10
|
+
setInterval(function() {
|
11
|
+
$("#content").load(action)
|
12
|
+
}, 1000 * poll_interval)
|
13
|
+
|
14
|
+
return false
|
15
|
+
})
|
16
|
+
})
|
@@ -0,0 +1,108 @@
|
|
1
|
+
/*
|
2
|
+
awesome buttons are based on a blog post by ZERB
|
3
|
+
Read there blog post for more information:
|
4
|
+
"Super awesome Buttons with CSS3 and RGBA":http://www.zurb.com/article/266/super-awesome-buttons-with-css3-and-rgba
|
5
|
+
|
6
|
+
this buttons are even more awesome, as the need only one color for all three states,
|
7
|
+
and have an super awesome onclick state
|
8
|
+
*/
|
9
|
+
|
10
|
+
/* set an awesome color for the buttons, feel free to add new colors like an .awesome.green or .awesome.secondary */
|
11
|
+
.awesome {
|
12
|
+
background-color: #5D6F72 !important;
|
13
|
+
color: #fff !important;
|
14
|
+
}
|
15
|
+
|
16
|
+
/* the awesome size gets set here. Feel free to add new sizes, like .awesome.small or .small.large */
|
17
|
+
.awesome { padding: 5px 10px 6px !important; font-size: 13px !important; }
|
18
|
+
.awesome:active { padding: 6px 10px 5px !important; }
|
19
|
+
|
20
|
+
.awesome.small { padding: 2px, 5px, 3px !important; font-size: 10px !important; }
|
21
|
+
|
22
|
+
/* Touch the rest at your onw risk. */
|
23
|
+
.awesome {
|
24
|
+
border: 0 !important;
|
25
|
+
cursor: pointer !important;
|
26
|
+
font-style: normal !important;
|
27
|
+
font-weight: bold !important;
|
28
|
+
font: inherit !important;
|
29
|
+
line-height: 1 !important;
|
30
|
+
position: relative !important;
|
31
|
+
text-align: center !important;
|
32
|
+
text-decoration: none !important;
|
33
|
+
|
34
|
+
/* vertical margin is the opposite of button's awesomeness */
|
35
|
+
margin-top: 0 !important;
|
36
|
+
margin-bottom: 0 !important;
|
37
|
+
|
38
|
+
/* not all browser support these, but who cares? */
|
39
|
+
text-shadow: 0 -1px 1px rgba(0,0,0,0.25), -2px 0 1px rgba(0,0,0,0.25) !important;
|
40
|
+
|
41
|
+
border-radius: 5px !important;
|
42
|
+
-moz-border-radius: 5px !important;
|
43
|
+
-webkit-border-radius: 5px !important;
|
44
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.5) !important;
|
45
|
+
-moz-box-shadow: 0 1px 2px rgba(0,0,0,0.5) !important;
|
46
|
+
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.5) !important;
|
47
|
+
|
48
|
+
/* who needs images these days? */
|
49
|
+
background-image: -moz-linear-gradient(top, rgba(255,255,255,.2), rgba(150,150,150,.2), rgba(0,0,0,.0)) !important;
|
50
|
+
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(255,255,255,.2)), color-stop(0.5, rgba(150,150,150,.2)), to(rgba(0,0,0,.0))) !important;
|
51
|
+
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#30ffffff,endColorstr=#10000000) progid:DXImageTransform.Microsoft.Shadow(color=#000000,direction=135,strength=2);
|
52
|
+
|
53
|
+
/* cross browser inline block hack
|
54
|
+
see http://blog.mozilla.com/webdev/2009/02/20/cross-browser-inline-block/ */
|
55
|
+
display: -moz-inline-stack;
|
56
|
+
display: inline-block;
|
57
|
+
vertical-align: middle !important;
|
58
|
+
*display: inline !important;
|
59
|
+
position: relative;
|
60
|
+
|
61
|
+
/* IE luv */
|
62
|
+
zoom: 1;
|
63
|
+
|
64
|
+
/* disable text selection (Firefox only) */
|
65
|
+
-moz-user-select: none;
|
66
|
+
}
|
67
|
+
|
68
|
+
/* OPERA only
|
69
|
+
if there is no border set, Opera shows a transparent background-color if border-radius is set. */
|
70
|
+
@media all and (-webkit-min-device-pixel-ratio:10000),not all and (-webkit-min-device-pixel-ratio:0) {
|
71
|
+
input.awesome {
|
72
|
+
border: 1px solid RGBa(0,0,0,0) !important;
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
/* hide selection background color */
|
77
|
+
.awesome::selection {
|
78
|
+
background: transparent;
|
79
|
+
}
|
80
|
+
|
81
|
+
.awesome {
|
82
|
+
outline: 0; /* remove dotted line, works for links only */
|
83
|
+
}
|
84
|
+
.awesome::-moz-focus-inner {
|
85
|
+
border: none; /* remove dotted lines for buttons */
|
86
|
+
}
|
87
|
+
.awesome:focus,
|
88
|
+
.awesome:hover {
|
89
|
+
background-image: -moz-linear-gradient(top, rgba(255,255,255,.4), rgba(150,150,150,.3), rgba(0,0,0,.0)) !important;
|
90
|
+
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(255,255,255,.4)), color-stop(0.5, rgba(150,150,150,.3)), to(rgba(0,0,0,.0))) !important;
|
91
|
+
#filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#50ffffff,endColorstr=#20000000) progid:DXImageTransform.Microsoft.Shadow(color=#000000,direction=135,strength=2);
|
92
|
+
}
|
93
|
+
.awesome:active {
|
94
|
+
background-image: -moz-linear-gradient(top, rgba(0,0,0,.2), rgba(150,150,150,.2), rgba(255,255,255,.2)) !important;
|
95
|
+
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(0,0,0,.2)), color-stop(0.5, rgba(150,150,150,.2)), to(rgba(255,255,255,.2))) !important;
|
96
|
+
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#20000000,endColorstr=#50ffffff) progid:DXImageTransform.Microsoft.Shadow(color=#000000,direction=315,strength=1);
|
97
|
+
|
98
|
+
box-shadow: inset 0 1px 2px rgba(0,0,0,0.7) !important;
|
99
|
+
-moz-box-shadow: inset 0 1px 2px rgba(0,0,0,0.7) !important;
|
100
|
+
-webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,0.7) !important;
|
101
|
+
}
|
102
|
+
|
103
|
+
/* Safari doesn't support inset box shadow, so we better remove it */
|
104
|
+
@media screen and (-webkit-min-device-pixel-ratio:0){
|
105
|
+
.awesome:active {
|
106
|
+
-webkit-box-shadow: none;
|
107
|
+
}
|
108
|
+
}
|