kthxbye 1.0.0
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.
- 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
|
+
}
|