kastner-clarity 0.9.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.
- data/History.txt +4 -0
- data/Manifest.txt +33 -0
- data/PostInstall.txt +8 -0
- data/README.rdoc +78 -0
- data/Rakefile +24 -0
- data/bin/clarity +10 -0
- data/config/config.yml.sample +7 -0
- data/lib/clarity.rb +23 -0
- data/lib/clarity/cli.rb +94 -0
- data/lib/clarity/commands/grep_command_builder.rb +56 -0
- data/lib/clarity/commands/tail_command_builder.rb +27 -0
- data/lib/clarity/grep_renderer.rb +37 -0
- data/lib/clarity/process_tree.rb +23 -0
- data/lib/clarity/renderers/log_renderer.rb +36 -0
- data/lib/clarity/server.rb +130 -0
- data/lib/clarity/server/basic_auth.rb +24 -0
- data/lib/clarity/server/chunk_http.rb +59 -0
- data/lib/clarity/server/mime_types.rb +23 -0
- data/public/images/spinner_big.gif +0 -0
- data/public/javascripts/app.js +154 -0
- data/public/stylesheets/app.css +54 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/test/commands/grep_command_builder_test.rb +99 -0
- data/test/commands/tail_command_builder_test.rb +42 -0
- data/test/files/logfile.log +17 -0
- data/test/test_helper.rb +4 -0
- data/test/test_string_scanner.rb +16 -0
- data/views/_header.html.erb +5 -0
- data/views/_toolbar.html.erb +64 -0
- data/views/error.html.erb +11 -0
- data/views/index.html.erb +9 -0
- metadata +144 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require 'cgi'
|
|
2
|
+
require File.dirname(__FILE__) + '/server/basic_auth'
|
|
3
|
+
require File.dirname(__FILE__) + '/server/mime_types'
|
|
4
|
+
require File.dirname(__FILE__) + '/server/chunk_http'
|
|
5
|
+
require File.dirname(__FILE__) + '/grep_renderer'
|
|
6
|
+
require File.dirname(__FILE__) + '/process_tree'
|
|
7
|
+
|
|
8
|
+
module Clarity
|
|
9
|
+
class NotFoundError < StandardError; end
|
|
10
|
+
class NotAuthenticatedError < StandardError; end
|
|
11
|
+
class InvalidParameterError < StandardError; end
|
|
12
|
+
|
|
13
|
+
module Server
|
|
14
|
+
include EventMachine::HttpServer
|
|
15
|
+
include Clarity::BasicAuth
|
|
16
|
+
include Clarity::ChunkHttp
|
|
17
|
+
|
|
18
|
+
attr_accessor :required_username, :required_password, :relative_root
|
|
19
|
+
attr_accessor :log_files
|
|
20
|
+
|
|
21
|
+
def self.run(options)
|
|
22
|
+
|
|
23
|
+
EventMachine::run do
|
|
24
|
+
EventMachine.epoll
|
|
25
|
+
EventMachine::start_server(options[:address], options[:port], self) do |a|
|
|
26
|
+
a.log_files = options[:log_files]
|
|
27
|
+
a.required_username = options[:username]
|
|
28
|
+
a.required_password = options[:password]
|
|
29
|
+
a.relative_root = options[:relative_root] || ""
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
STDERR.puts "Clarity #{Clarity::VERSION} starting up."
|
|
33
|
+
STDERR.puts " * listening on #{options[:address]}:#{options[:port]}"
|
|
34
|
+
|
|
35
|
+
if options[:user]
|
|
36
|
+
STDERR.puts " * Running as user #{options[:user]}"
|
|
37
|
+
EventMachine.set_effective_user(options[:user])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
STDERR.puts " * Log mask(s): #{options[:log_files].join(', ')}"
|
|
41
|
+
STDERR.puts
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def process_http_request
|
|
46
|
+
authenticate!
|
|
47
|
+
|
|
48
|
+
puts "action: #{path}"
|
|
49
|
+
puts "params: #{params.inspect}"
|
|
50
|
+
|
|
51
|
+
case path
|
|
52
|
+
when '/'
|
|
53
|
+
respond_with(200, welcome_page)
|
|
54
|
+
|
|
55
|
+
when '/perform'
|
|
56
|
+
if params.empty?
|
|
57
|
+
respond_with(200, welcome_page)
|
|
58
|
+
else
|
|
59
|
+
# get command
|
|
60
|
+
command = case params['tool']
|
|
61
|
+
when 'grep' then GrepCommandBuilder.new(params).command
|
|
62
|
+
when 'tail' then TailCommandBuilder.new(params).command
|
|
63
|
+
else raise InvalidParameterError, "Invalid Tool parameter"
|
|
64
|
+
end
|
|
65
|
+
response = respond_with_chunks
|
|
66
|
+
response.chunk results_page # display page header
|
|
67
|
+
|
|
68
|
+
puts "Running: #{command}"
|
|
69
|
+
|
|
70
|
+
EventMachine::popen(command, GrepRenderer) do |grepper|
|
|
71
|
+
@grepper = grepper
|
|
72
|
+
@grepper.response = response
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
when '/test'
|
|
77
|
+
response = respond_with_chunks
|
|
78
|
+
EventMachine::add_periodic_timer(1) do
|
|
79
|
+
response.chunk "Lorem ipsum dolor sit amet<br/>"
|
|
80
|
+
response.send_chunks
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
else
|
|
84
|
+
respond_with(200, public_file(path), :content_type => Mime.for(path))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
rescue InvalidParameterError => e
|
|
88
|
+
respond_with(500, error_page(e))
|
|
89
|
+
rescue NotFoundError => e
|
|
90
|
+
respond_with(404, "<h1>Not Found</h1>")
|
|
91
|
+
rescue NotAuthenticatedError => e
|
|
92
|
+
puts "Could not authenticate user"
|
|
93
|
+
headers = { "WWW-Authenticate" => %(Basic realm="Clarity")}
|
|
94
|
+
respond_with(401, "HTTP Basic: Access denied.\n", :content_type => 'text/plain', :headers => headers)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def error_page(error)
|
|
98
|
+
@error = error
|
|
99
|
+
render "error.html.erb"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def welcome_page
|
|
103
|
+
render "index.html.erb"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def results_page
|
|
107
|
+
render "index.html.erb"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def unbind
|
|
111
|
+
@grepper.close_connection if @grepper
|
|
112
|
+
close_connection
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def authenticate!
|
|
118
|
+
login, pass = authentication_data
|
|
119
|
+
|
|
120
|
+
if (required_username && required_username != login) || (required_password && required_password != pass)
|
|
121
|
+
raise NotAuthenticatedError
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
true
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Clarity
|
|
2
|
+
module BasicAuth
|
|
3
|
+
|
|
4
|
+
def decode_credentials(request)
|
|
5
|
+
Base64.decode64(request).split.last
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def user_name_and_password(request)
|
|
9
|
+
decode_credentials(request).split(/:/, 2)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def authentication_data
|
|
13
|
+
headers = @http_headers.split("\000")
|
|
14
|
+
auth_header = headers.detect {|head| head =~ /Authorization: / }
|
|
15
|
+
header = auth_header.nil? ? "" : auth_header.split("Authorization: Basic ").last
|
|
16
|
+
return (user_name_and_password(header) rescue ['', ''])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def authenticate!(http_header)
|
|
20
|
+
raise NotAuthenticatedError unless authenticate(http_header)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require 'erb'
|
|
2
|
+
|
|
3
|
+
module Clarity
|
|
4
|
+
|
|
5
|
+
module ChunkHttp
|
|
6
|
+
|
|
7
|
+
LeadIn = ' ' * 1024
|
|
8
|
+
|
|
9
|
+
def respond_with_chunks
|
|
10
|
+
response = EventMachine::DelegatedHttpResponse.new( self )
|
|
11
|
+
response.status = 200
|
|
12
|
+
response.headers['Content-Type'] = 'text/html'
|
|
13
|
+
response.chunk LeadIn
|
|
14
|
+
response
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def respond_with(status, content, options = {})
|
|
18
|
+
response = EventMachine::DelegatedHttpResponse.new( self )
|
|
19
|
+
response.headers['Content-Type'] = options.fetch(:content_type, 'text/html')
|
|
20
|
+
response.headers['Cache-Control'] = 'private, max-age=0'
|
|
21
|
+
headers = options.fetch(:headers, {})
|
|
22
|
+
headers.each_pair {|h, v| response.headers[h] = v }
|
|
23
|
+
response.status = status
|
|
24
|
+
response.content = content
|
|
25
|
+
response.send_response
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render(view)
|
|
29
|
+
@toolbar = template("_toolbar.html.erb")
|
|
30
|
+
@content_for_header = template("_header.html.erb")
|
|
31
|
+
template(view)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def template(filename)
|
|
35
|
+
content = File.read( File.join(Clarity::Templates, filename) )
|
|
36
|
+
ERB.new(content).result(binding)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def public_file(filename)
|
|
40
|
+
File.read( File.join(Clarity::Public, filename) )
|
|
41
|
+
rescue Errno::ENOENT
|
|
42
|
+
raise NotFoundError
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def logfiles
|
|
46
|
+
log_files.map {|f| Dir[f] }.flatten.compact.uniq.select{|f| File.file?(f) }.sort
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def params
|
|
50
|
+
ENV['QUERY_STRING'].split('&').inject({}) {|p,s| k,v = s.split('=');p[k.to_s] = CGI.unescape(v.to_s);p}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def path
|
|
54
|
+
ENV["PATH_INFO"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Clarity
|
|
2
|
+
module Mime
|
|
3
|
+
def self.for(filename)
|
|
4
|
+
|
|
5
|
+
content_type = TYPES[File.extname(filename)]
|
|
6
|
+
content_type || 'text/plain'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
TYPES = {
|
|
10
|
+
'.jpg' => 'image/jpg',
|
|
11
|
+
'.jpeg' => 'image/jpeg',
|
|
12
|
+
'.gif' => 'image/gif',
|
|
13
|
+
'.png' => 'image/png',
|
|
14
|
+
'.bmp' => 'image/bmp',
|
|
15
|
+
'.bitmap' => 'image/x-ms-bmp',
|
|
16
|
+
'.js' => 'application/javascript',
|
|
17
|
+
'.txt' => 'text/plain',
|
|
18
|
+
'.css' => 'text/css',
|
|
19
|
+
'.html' => 'text/html',
|
|
20
|
+
'.htm' => 'text/html'
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* ----------------------------------------------------------------------------
|
|
2
|
+
* Log Search Server JS
|
|
3
|
+
* by John Tajima
|
|
4
|
+
* requires jQuery 1.3.2
|
|
5
|
+
* ----------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
var Search = {
|
|
10
|
+
search_form : 'search', // domId of the form
|
|
11
|
+
resultsId : 'results',
|
|
12
|
+
search_fields: [ 'term1', 'term2', 'term3' ], // domIds of search term fields
|
|
13
|
+
file_list : 'file-list', // domId of select for logfiles
|
|
14
|
+
logfiles : {}, // hash of log files
|
|
15
|
+
past_params : null, // recent request
|
|
16
|
+
url : '/perform',
|
|
17
|
+
scroll_fnId : null,
|
|
18
|
+
|
|
19
|
+
// initialize Search form
|
|
20
|
+
// { 'grep': [ log, files, for, grep], 'tail': [ 'log', 'files', 'for', 'tail']}
|
|
21
|
+
init: function(logfiles, params) {
|
|
22
|
+
this.logfiles = logfiles;
|
|
23
|
+
this.past_params = params;
|
|
24
|
+
|
|
25
|
+
this.bind_grep_tool();
|
|
26
|
+
this.bind_tail_tool();
|
|
27
|
+
this.bind_options();
|
|
28
|
+
|
|
29
|
+
if (!this.past_params) return; // return if no prev settings, nothing to set
|
|
30
|
+
|
|
31
|
+
// init tool selector
|
|
32
|
+
(this.past_params['tool'] == 'grep') ? $('#grep-label').trigger('click') : $('#tail-tool').trigger('click');
|
|
33
|
+
|
|
34
|
+
// init log file selector
|
|
35
|
+
$('#'+this.file_list).val(this.past_params['file']);
|
|
36
|
+
|
|
37
|
+
// init search fields
|
|
38
|
+
jQuery.each(this.search_fields, function(){
|
|
39
|
+
$('#'+this).val(Search.past_params[this]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// advanced options usd?
|
|
43
|
+
// time was set - so show advanced options
|
|
44
|
+
if ((this.past_params['sh']) || (this.past_params['eh'])) {
|
|
45
|
+
this.showAdvanced();
|
|
46
|
+
if (this.past_params['sh']) {
|
|
47
|
+
jQuery.each(['sh', 'sm', 'ss'], function(){ $('#'+this).val(Search.past_params[this]) });
|
|
48
|
+
}
|
|
49
|
+
if (this.past_params['eh']) {
|
|
50
|
+
jQuery.each(['eh', 'em', 'es'], function(){ $('#'+this).val(Search.past_params[this]) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// bind option selectors
|
|
57
|
+
bind_options: function() {
|
|
58
|
+
$('#auto-scroll').bind('change', function(){
|
|
59
|
+
Search.autoScroll(this.checked);
|
|
60
|
+
});
|
|
61
|
+
$('#auto-scroll').attr('checked', true).trigger('change'); // by default, turn on
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// bind change grep tool
|
|
65
|
+
bind_grep_tool: function() {
|
|
66
|
+
$('#grep-tool').bind('change', function(e){
|
|
67
|
+
var newlist = ""
|
|
68
|
+
jQuery.each(Search.logfiles['grep'], function(){
|
|
69
|
+
newlist += "<option value='" + this + "'>" + this + "</option>\n"
|
|
70
|
+
});
|
|
71
|
+
$('#'+Search.file_list).html(newlist);
|
|
72
|
+
});
|
|
73
|
+
// watch clicking label as well
|
|
74
|
+
$('#grep-label').bind('click', function(e){
|
|
75
|
+
$('#grep-tool').attr('checked', 'checked').val('grep').trigger('change');
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
// bind change tail tool
|
|
81
|
+
bind_tail_tool: function() {
|
|
82
|
+
$('#tail-tool').bind('change', function(e){
|
|
83
|
+
var newlist = ""
|
|
84
|
+
jQuery.each(Search.logfiles['tail'], function(){
|
|
85
|
+
newlist += "<option value='" + this + "'>" + this + "</option>\n"
|
|
86
|
+
});
|
|
87
|
+
$('#'+ Search.file_list).html(newlist);
|
|
88
|
+
});
|
|
89
|
+
// watch clicking label as well
|
|
90
|
+
$('#tail-label').bind('click', function(e){
|
|
91
|
+
$('#tail-tool').attr('checked', 'checked').val('tail').trigger('change');
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
// clears the terms fields
|
|
97
|
+
clear: function() {
|
|
98
|
+
jQuery.each(this.search_fields, function(){
|
|
99
|
+
$('#'+this).val("");
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
showAdvanced: function() {
|
|
104
|
+
$('#enable-advanced').hide();
|
|
105
|
+
$('#disable-advanced').show();
|
|
106
|
+
$('.advanced-options').show();
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
hideAdvanced: function() {
|
|
110
|
+
this.clearAdvanced();
|
|
111
|
+
$('#enable-advanced').show();
|
|
112
|
+
$('#disable-advanced').hide();
|
|
113
|
+
$('.advanced-options').hide();
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
clearAdvanced: function() {
|
|
117
|
+
$('#advanced-options input').val("");
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// gathers form elements and submits to proper url
|
|
121
|
+
submit: function() {
|
|
122
|
+
$('#'+this.search_form).submit();
|
|
123
|
+
$('#'+this.resultsId).html("Sending new query...");
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
//
|
|
127
|
+
// Misc utitilies
|
|
128
|
+
//
|
|
129
|
+
|
|
130
|
+
autoScroll: function(enabled) {
|
|
131
|
+
if (enabled == true) {
|
|
132
|
+
if (this.scroll_fnId) return; // already running
|
|
133
|
+
|
|
134
|
+
//console.log("scroll ON!")
|
|
135
|
+
window._currPos = 0; // init pos
|
|
136
|
+
this.scroll_fnId = setInterval( function(){
|
|
137
|
+
if (window._currPos < document.height) {
|
|
138
|
+
window.scrollTo(0, document.height);
|
|
139
|
+
window._currPos = document.height;
|
|
140
|
+
}
|
|
141
|
+
}, 100 );
|
|
142
|
+
} else {
|
|
143
|
+
if (!this.scroll_fnId) return;
|
|
144
|
+
//console.log("scroll off")
|
|
145
|
+
if (this.scroll_fnId) {
|
|
146
|
+
clearInterval(this.scroll_fnId);
|
|
147
|
+
window._currPost = 0;
|
|
148
|
+
this.scroll_fnId = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
};
|
|
154
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* ----------------------------------------------------------------------------
|
|
2
|
+
* Log Search Server CSS
|
|
3
|
+
* by John Tajima
|
|
4
|
+
* ----------------------------------------------------------------------------
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* 960 reset */
|
|
8
|
+
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}:focus{outline:0}ins{text-decoration:none}del{text-decoration:line-through}table{border-collapse:collapse;border-spacing:0}
|
|
9
|
+
/* 960 text */
|
|
10
|
+
body{font:13px/1.5 'Helvetica Neue',Arial,'Liberation Sans',FreeSans,sans-serif}a:focus{outline:1px dotted invert}hr{border:0 #ccc solid;border-top-width:1px;clear:both;height:0}h1{font-size:25px}h2{font-size:23px}h3{font-size:21px}h4{font-size:19px}h5{font-size:17px}h6{font-size:15px}ol{list-style:decimal}ul{list-style:disc}li{margin-left:30px}p,dl,hr,h1,h2,h3,h4,h5,h6,ol,ul,pre,table,address,fieldset{margin-bottom:20px}
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Toolbar Styling
|
|
14
|
+
*
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
#toolbar { color: #333; font-size: 12px; min-width: 980px; height: 130px;
|
|
19
|
+
padding: 0 10px; background: #f0f0f0; border-bottom: 2px solid #000;
|
|
20
|
+
position:fixed; top: 0; margin: 0 -10px; width: 100%;
|
|
21
|
+
font-family: 'Helvetica Neue',Arial,'Liberation Sans',FreeSans,sans-serif;
|
|
22
|
+
}
|
|
23
|
+
#toolbar a { color: blue; background: none; }
|
|
24
|
+
#toolbar a:hover { background: none; }
|
|
25
|
+
|
|
26
|
+
#toolbar #header { height: 20px; padding: 3px 5px; margin: 0 -10px 10px; overflow:hidden; background: #333; border-bottom: 2px solid #222; color: #eee; }
|
|
27
|
+
#toolbar #header h1 { font-size: 16px; line-height: 20px; }
|
|
28
|
+
#toolbar #header a { text-decoration: none; color: #eee; }
|
|
29
|
+
#toolbar #header a:hover { text-decoration: none; color: #fff;}
|
|
30
|
+
#toolbar .small { font-size: 11px; color: #333; font-weight: bold;}
|
|
31
|
+
#toolbar input.time { width: 20px;}
|
|
32
|
+
|
|
33
|
+
table.actions { width: auto; border-collapse: collapse; }
|
|
34
|
+
table.actions th { font-size: 11px; color: #333; text-align: left; min-width: 100px; padding: 0 5px; }
|
|
35
|
+
table.actions td { padding: 2px 5px; text-align: left; vertical-align: middle;}
|
|
36
|
+
table.actions span.note { font-weight: normal; color: #999; }
|
|
37
|
+
table.actions span.and { font-size: 11px; color: red; font-weight: bold; }
|
|
38
|
+
table.actions span.label { font-weight: bold; cursor:pointer;}
|
|
39
|
+
table.actions input[type=text] { width: 200px; padding: 2px; border: 1px solid #aaa;}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
div#option-ctrl { position: fixed; z-index: 100; bottom: 0; right: 0; min-width: 60px; min-height: 20px; height: 20px; padding: 5px; border: 1px solid #ddd; background: #f0f0f0; font-size: 11px; color: #000;
|
|
43
|
+
font-family: 'Helvetica Neue',Arial,'Liberation Sans',FreeSans,sans-serif; }
|
|
44
|
+
div#option-ctrl ul { list-style:none; margin: 0; padding: 0;}
|
|
45
|
+
div#option-ctrl ul li { list-style:none; margin: 0 5px 0 0; float:left; }
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/* output */
|
|
50
|
+
body { margin: 10px; padding-top: 130px; font-family: 'Monaco', 'Deja Vu Sans Mono', 'Inconsolata' ,'Consolas',monospace; background:#111 none repeat scroll 0 0; color:#fff; font-size:10px;}
|
|
51
|
+
a { color: #0f0; }
|
|
52
|
+
a:hover { background-color: #03c; color: white; text-decoration: none; }
|
|
53
|
+
|
|
54
|
+
|