├── data └── .gitkeep ├── views ├── docs │ └── api.slim ├── dump.slim ├── index.slim ├── layout.slim ├── slow_query.slim ├── list.slim ├── dumped_query.slim └── view.slim ├── .rspec ├── config.sample.toml ├── lib ├── nata2.rb └── nata2 │ ├── version.rb │ ├── config.rb │ ├── helpers.rb │ ├── mysqldumpslow.rb │ ├── data.rb │ └── server.rb ├── Gemfile ├── public ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.svg ├── css │ ├── morris.css │ ├── prettify-tommorow.css │ └── bootstrap-theme.min.css └── js │ ├── lang-sql.js │ ├── prettify.js │ ├── bootstrap.min.js │ └── morris.min.js ├── Rakefile ├── config.ru ├── spec ├── nata2 │ ├── data_spec.rb │ └── server_spec.rb └── spec_helper.rb ├── .travis.yml ├── bin └── migrate_data_for_1_0_0 ├── .gitignore ├── nata2.gemspec ├── Schemafile └── README.md /data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/docs/api.slim: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour --format d 2 | -------------------------------------------------------------------------------- /config.sample.toml: -------------------------------------------------------------------------------- 1 | dburl = "sqlite://data/nata2.db" 2 | -------------------------------------------------------------------------------- /lib/nata2.rb: -------------------------------------------------------------------------------- 1 | require 'nata2/version' 2 | 3 | module Nata2 4 | end 5 | -------------------------------------------------------------------------------- /lib/nata2/version.rb: -------------------------------------------------------------------------------- 1 | module Nata2 2 | VERSION = '1.0.0' 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in nata2.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/studio3104/nata2/HEAD/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/studio3104/nata2/HEAD/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new('spec') 5 | task :default => :spec 6 | -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/studio3104/nata2/HEAD/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib')) 2 | require 'sinatra' 3 | require 'nata2/server' 4 | 5 | run Nata2::Server 6 | -------------------------------------------------------------------------------- /spec/nata2/data_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nata2/data' 3 | 4 | describe Nata2::Data do 5 | let(:data) { Nata2::Data.new } 6 | end 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 2.0.0 5 | - 2.1.1 6 | 7 | before_install: 8 | - gem update bundler 9 | 10 | script: 11 | - bundle exec rake spec 12 | -------------------------------------------------------------------------------- /bin/migrate_data_for_1_0_0: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), *%w[.. lib]) 3 | require 'nata2/data' 4 | data = Nata2::Data.new 5 | data.migrate_data_for_1_0_0 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | *.db 4 | .bundle 5 | .config 6 | config.toml 7 | .yardoc 8 | Gemfile.lock 9 | InstalledFiles 10 | _yardoc 11 | coverage 12 | doc/ 13 | lib/bundler/man 14 | pkg 15 | rdoc 16 | spec/reports 17 | test/tmp 18 | test/version_tmp 19 | tmp 20 | -------------------------------------------------------------------------------- /public/css/morris.css: -------------------------------------------------------------------------------- 1 | .morris-hover{position:absolute;z-index:1000;}.morris-hover.morris-default-style{border-radius:10px;padding:6px;color:#666;background:rgba(255, 255, 255, 0.8);border:solid 2px rgba(230, 230, 230, 0.8);font-family:sans-serif;font-size:12px;text-align:center;}.morris-hover.morris-default-style .morris-hover-row-label{font-weight:bold;margin:0.25em 0;} 2 | .morris-hover.morris-default-style .morris-hover-point{white-space:nowrap;margin:0.1em 0;} 3 | -------------------------------------------------------------------------------- /lib/nata2/config.rb: -------------------------------------------------------------------------------- 1 | require 'nata2' 2 | require 'toml' 3 | 4 | config_file = File.expand_path('config.toml', "#{__dir__}/../..") 5 | CONFIG = File.exist?(config_file) ? TOML.load_file(config_file) : {} 6 | 7 | class Nata2::Config 8 | @@dburl = nil 9 | def self.get(keyword) 10 | case keyword 11 | when :dburl 12 | return @@dburl if @@dburl 13 | if ENV['RACK_ENV'] == 'test' 14 | require 'tempfile' 15 | temp = Tempfile.new('nata2test.db') 16 | @@dburl = %Q{sqlite://#{temp.path}} 17 | else 18 | @@dburl = CONFIG['dburl'] || %Q[sqlite://#{File.dirname(__FILE__)}/../../data/nata2.db] 19 | end 20 | else 21 | raise ArgumentError, "unknown configuration keyword: #{keyword}" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /views/dump.slim: -------------------------------------------------------------------------------- 1 | p style="float: right;" [ 合計値 (平均値) ] 2 | table.table.table-hover.table-condensed 3 | tr 4 | th width="54%;" Query 5 | th.text-right width="2%;" count 6 | th.text-right width="12%;" query time 7 | th.text-right width="9%;" lock time 8 | th.text-right width="13%;" rows sent 9 | 10 | - @slow_queries.each do |query| 11 | tr 12 | td 13 | a target="_blank" href="/dumped_query/#{Base64.strict_encode64(JSON.generate(query))}" 14 | - cut_sql = query[:normarized_sql].gsub(/\n/, ' ').gsub(/\s+/, ' ') 15 | - cut_sql = cut_sql[0, 90] + '...' if cut_sql.bytesize > 90 16 | #{cut_sql} 17 | td align="right" 18 | #{query[:count]} 19 | td align="right" 20 | #{sprintf("%0.2f", query[:summation][:query_time])} (#{sprintf("%0.2f", query[:average][:query_time])}) s 21 | td align="right" 22 | #{sprintf("%0.2f", query[:summation][:lock_time])} (#{sprintf("%0.2f", query[:average][:lock_time])}) s 23 | td align="right" 24 | #{query[:summation][:rows_sent]} (#{sprintf("%0.2f", query[:average][:rows_sent])}) 25 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RACK_ENV'] = 'test' 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | require 'nata2/server' 4 | require 'nata2/data' 5 | require 'nata2/config' 6 | require 'uri' 7 | require 'rspec' 8 | require 'rack/test' 9 | 10 | module RSpecMixin 11 | include Rack::Test::Methods 12 | include Nata2::Helpers 13 | def app() Nata2::Server end 14 | end 15 | 16 | RSpec.configure do |c| 17 | c.include RSpecMixin 18 | end 19 | 20 | class TestData 21 | ServiceName = 'nataapplication' 22 | HostName = 'nata.db01' 23 | DatabaseName = 'nata_db' 24 | ParsedSlowQuery = { 25 | datetime: 1390883951, user: 'user', host: 'localhost', 26 | query_time: 2.001227, lock_time: 0.0, rows_sent: 1, rows_examined:0, 27 | sql: 'select sleep(2)' 28 | } 29 | end 30 | 31 | path_to_db = URI.parse(Nata2::Config.get(:dburl)).path 32 | Dir.chdir(File.join(File.dirname(__FILE__), '..')) { 33 | system(%Q[bundle exec ridgepole -c "{adapter: sqlite3, database: #{path_to_db}}" --apply]) 34 | } 35 | data = Nata2::Data.new 36 | data.register_slow_query(TestData::ServiceName, TestData::HostName, TestData::DatabaseName, TestData::ParsedSlowQuery) 37 | -------------------------------------------------------------------------------- /views/index.slim: -------------------------------------------------------------------------------- 1 | - @bundles.each do |service_name, databases| 2 | .panel-group id="accordion_#{service_name}" 3 | .panel.panel-default 4 | .panel-heading 5 | h4.panel-title 6 | a data-toggle="collapse" data-parent="#accordion_#{service_name}" href="#collapse_#{service_name}" 7 | span.glyphicon.glyphicon-collapse-down 8 | span #{service_name} 9 | .panel-collapse.collapse.in id="collapse_#{service_name}" 10 | .panel-body 11 | - if !@complex[service_name].empty? 12 | b 13 | | Complex 14 | ul.list-unstyled 15 | - @complex[service_name].each do |database_name| 16 | li 17 | span 18 | | ●   19 | span 20 | a href="/view_complex/#{service_name}/#{database_name}" #{database_name} 21 | 22 | b 23 | | Database 24 | ul.list-unstyled 25 | - databases.each do |database| 26 | li 27 | span 28 | font color="#{database[:color]}" ● 29 | span 30 | |    31 | span 32 | a href="/view/#{service_name}/#{database[:host]}/#{database[:database]}" #{database[:database]} 33 | span  ( 34 | span #{database[:host]} 35 | span ) 36 | -------------------------------------------------------------------------------- /views/layout.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html lang="en" 3 | head 4 | meta charset="UTF-8" 5 | link rel="stylesheet" type="text/css" href="/css/bootstrap-theme.min.css" 6 | link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css" 7 | link rel="stylesheet" type="text/css" href="/css/prettify-tommorow.css" 8 | link rel="stylesheet" type="text/css" href="/css/morris.css" 9 | css: 10 | body { padding-top: 70px; } 11 | 12 | body 13 | header.navbar.navbar-default.navbar-fixed-top.bs-docs-nav role="banner" 14 | .container 15 | .navbar-header 16 | a.navbar-brand 鉈 17 | nav.collapse.navbar-collapse.bs-navbar-collapse role="navigation" 18 | ul.nav.navbar-nav 19 | li 20 | a href="/" データベース選択 21 | li 22 | li 23 | ul.nav.navbar-nav.navbar-right 24 | li 25 | a href="/docs/api" APIドキュメント 26 | 27 | .container 28 | .row 29 | == yield 30 | 31 | script type="text/javascript" src="/js/jquery.min.js" 32 | script type="text/javascript" src="/js/raphael-min.js" 33 | script type="text/javascript" src="/js/morris.min.js" 34 | script type="text/javascript" src="/js/bootstrap.min.js" 35 | script type="text/javascript" src="/js/prettify.js" 36 | script type="text/javascript" src="/js/lang-sql.js" 37 | javascript: 38 | prettyPrint(); 39 | -------------------------------------------------------------------------------- /views/slow_query.slim: -------------------------------------------------------------------------------- 1 | blockquote style="border-color : #FFFFFF;" 2 | h6 3 | span.label.label-default #{@slow_query[:service_name]} 4 | |   5 | a href="/view/#{@slow_query[:service_name]}/#{@slow_query[:host_name]}/#{@slow_query[:database_name]}" 6 | span.label style="background-color : #{@slow_query[:color]}" #{@slow_query[:database_name]}(#{@slow_query[:host_name]}) 7 | h6 8 | - start_unixtime = (@slow_query[:datetime].to_f - @slow_query[:query_time]).ceil 9 | - start_datetime = Time.at(start_unixtime).strftime('%Y/%m/%d %H:%M:%S') 10 | - end_datetime = Time.at(@slow_query[:datetime]).strftime('%Y/%m/%d %H:%M:%S') 11 | span 12 | b #{@slow_query[:user]} @ #{@slow_query[:host]} 13 | span style="float: right;" 14 | b #{start_datetime} - #{end_datetime} 15 | h6 16 | table.table.table-striped.table-condensed 17 | tr 18 | th.text-right width="25%;" query time 19 | th.text-right width="25%;" lock time 20 | th.text-right width="25%;" rows sent 21 | th.text-right width="25%;" rows examined 22 | tr 23 | td align="right" #{sprintf("%0.2f", @slow_query[:query_time])} s 24 | td align="right" #{sprintf("%0.2f", @slow_query[:lock_time])} s 25 | td align="right" #{@slow_query[:rows_sent]} 26 | td align="right" #{@slow_query[:rows_examined]} 27 | pre.prettyprint.lang-sql 28 | #{@slow_query[:sql]} 29 | -------------------------------------------------------------------------------- /nata2.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'nata2/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'nata2' 8 | spec.version = Nata2::VERSION 9 | spec.authors = ['studio3104'] 10 | spec.email = ['studio3104.com@gmail.com'] 11 | spec.summary = %q{Analyzer of MySQL slow query log} 12 | spec.description = %q{Analyzer of MySQL slow query log} 13 | spec.homepage = '' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '~> 1.5' 22 | spec.add_development_dependency 'rake' 23 | spec.add_development_dependency 'rspec' 24 | spec.add_development_dependency 'webmock' 25 | spec.add_runtime_dependency 'sinatra' 26 | spec.add_runtime_dependency 'sinatra-contrib' 27 | spec.add_runtime_dependency 'slim', '= 2.0.2' 28 | spec.add_runtime_dependency 'toml' 29 | spec.add_runtime_dependency 'sequel' 30 | spec.add_runtime_dependency 'sqlite3' 31 | spec.add_runtime_dependency 'mysql2', '~> 0.3.20' 32 | spec.add_runtime_dependency 'ridgepole' 33 | spec.add_runtime_dependency 'focuslight-validator' 34 | end 35 | -------------------------------------------------------------------------------- /views/list.slim: -------------------------------------------------------------------------------- 1 | - @slow_queries_per_day.each do |day, slow_queries| 2 | h4 #{day} 3 | table.table.table-hover.table-condensed 4 | tr 5 | th width="3%;" 6 | th width="7%;" Time 7 | th width="54%;" Query 8 | th.text-right width="8%;" query time 9 | th.text-right width="8%;" lock time 10 | th.text-right width="10%;" rows sent 11 | th.text-right width="10%;" rows examined 12 | 13 | - slow_queries.each do |query| 14 | tr 15 | td align="center" 16 | a href="/view/#{query[:service_name]}/#{query[:host_name]}/#{query[:database_name]}" 17 | font color="#{query[:color]}" ● 18 | td #{Time.at(query[:datetime]).strftime('%H:%M:%S')} 19 | td 20 | a target="_blank" href='/slow_query/#{query[:id]}' 21 | - cut_sql = query[:sql].gsub(/\n/, ' ').gsub(/\s+/, ' ') 22 | - cut_sql = cut_sql[0, 80] + '...' if cut_sql.bytesize > 80 23 | #{cut_sql} 24 | td align="right" #{sprintf("%0.2f", query[:query_time])} 25 | td align="right" #{sprintf("%0.2f", query[:lock_time])} 26 | td align="right" #{query[:rows_sent]} 27 | td align="right" #{query[:rows_examined]} 28 | 29 | ul.pager 30 | - if @page == 1 31 | li.disabled 32 | a Newer 33 | - else 34 | li 35 | a href="?#{URI.encode_www_form(@params.merge(page: @page-1))}" Newer 36 | - if @disabled_next 37 | li.disabled 38 | a Older 39 | - else 40 | li 41 | a href="?#{URI.encode_www_form(@params.merge(page: @page+1))}" Older 42 | -------------------------------------------------------------------------------- /public/js/lang-sql.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/,null,"\"'"]],[["com",/^(?:--[^\n\r]*|\/\*[\S\s]*?(?:\*\/|$))/],["kwd",/^(?:add|all|alter|and|any|apply|as|asc|authorization|backup|begin|between|break|browse|bulk|by|cascade|case|check|checkpoint|close|clustered|coalesce|collate|column|commit|compute|connect|constraint|contains|containstable|continue|convert|create|cross|current|current_date|current_time|current_timestamp|current_user|cursor|database|dbcc|deallocate|declare|default|delete|deny|desc|disk|distinct|distributed|double|drop|dummy|dump|else|end|errlvl|escape|except|exec|execute|exists|exit|fetch|file|fillfactor|following|for|foreign|freetext|freetexttable|from|full|function|goto|grant|group|having|holdlock|identity|identitycol|identity_insert|if|in|index|inner|insert|intersect|into|is|join|key|kill|left|like|lineno|load|match|matched|merge|natural|national|nocheck|nonclustered|nocycle|not|null|nullif|of|off|offsets|on|open|opendatasource|openquery|openrowset|openxml|option|or|order|outer|over|partition|percent|pivot|plan|preceding|precision|primary|print|proc|procedure|public|raiserror|read|readtext|reconfigure|references|replication|restore|restrict|return|revoke|right|rollback|rowcount|rowguidcol|rows?|rule|save|schema|select|session_user|set|setuser|shutdown|some|start|statistics|system_user|table|textsize|then|to|top|tran|transaction|trigger|truncate|tsequal|unbounded|union|unique|unpivot|update|updatetext|use|user|using|values|varying|view|waitfor|when|where|while|with|within|writetext|xml)(?=[^\w-]|$)/i, 2 | null],["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],["pln",/^[_a-z][\w-]*/i],["pun",/^[^\w\t\n\r "'\xa0][^\w\t\n\r "'+\xa0-]*/]]),["sql"]); 3 | -------------------------------------------------------------------------------- /views/dumped_query.slim: -------------------------------------------------------------------------------- 1 | blockquote style="border-color : #FFFFFF;" 2 | pre.prettyprint.lang-sql 3 | #{@dumped_query[:normarized_sql]} 4 | h6 5 | table.table.table-bordered.table-striped.table-condensed 6 | tr 7 | th count 8 | th colspan="2" query time 9 | th colspan="2" lock time 10 | th colspan="2" rows sent 11 | th colspan="2" rows examined 12 | tr 13 | td 14 | span 15 | b total 16 | span style="float: right;" #{@dumped_query[:count]} 17 | td width="11%;" 18 | span 19 | b total 20 | span style="float: right;" #{sprintf("%0.2f", @dumped_query[:summation][:query_time])} s 21 | td width="11%;" 22 | span 23 | b avg 24 | span style="float: right;" #{sprintf("%0.2f", @dumped_query[:average][:query_time])} s 25 | td width="11%;" 26 | span 27 | b total 28 | span style="float: right;" #{sprintf("%0.2f", @dumped_query[:summation][:lock_time])} s 29 | td width="11%;" 30 | span 31 | b avg 32 | span style="float: right;" #{sprintf("%0.2f", @dumped_query[:average][:lock_time])} s 33 | td width="11%;" 34 | span 35 | b total 36 | span style="float: right;" #{@dumped_query[:summation][:rows_sent]} 37 | td width="11%;" 38 | span 39 | b avg 40 | span style="float: right;" #{sprintf("%0.2f", @dumped_query[:average][:rows_sent])} 41 | td width="11%;" 42 | span 43 | b total 44 | span style="float: right;" #{@dumped_query[:summation][:rows_examined]} 45 | td width="11%;" 46 | span 47 | b avg 48 | span style="float: right;" #{sprintf("%0.2f", @dumped_query[:average][:rows_examined])} 49 | -------------------------------------------------------------------------------- /Schemafile: -------------------------------------------------------------------------------- 1 | create_table "bundles", force: :cascade do |t| 2 | t.string "service_name", limit: 255, null: false 3 | t.string "host_name", limit: 255, null: false 4 | t.string "database_name", limit: 255, null: false 5 | t.string "color", limit: 255, null: false 6 | t.integer "created_at", null: false 7 | t.integer "updated_at", null: false 8 | end 9 | 10 | add_index "bundles", ["service_name", "host_name", "database_name"], name: "bundles_dbname_index", unique: true 11 | 12 | create_table "explains", force: :cascade do |t| 13 | t.integer "slow_query_id", limit: 8 14 | t.integer "explain_id", null: false 15 | t.string "select_type", limit: 255, null: false 16 | t.string "table", limit: 255, null: false 17 | t.string "type", limit: 255, null: false 18 | t.string "possible_keys", limit: 255 19 | t.string "key", limit: 255 20 | t.integer "key_len" 21 | t.string "ref", limit: 255 22 | t.integer "rows", limit: 8, null: false 23 | t.text "extra" 24 | t.integer "created_at", null: false 25 | t.integer "updated_at", null: false 26 | end 27 | 28 | create_table "slow_queries", force: :cascade do |t| 29 | t.integer "bundle_id", limit: 8 30 | t.integer "datetime" 31 | t.integer "period_per_hour" 32 | t.integer "period_per_day" 33 | t.string "user", limit: 255 34 | t.string "host", limit: 255 35 | t.float "query_time" 36 | t.float "lock_time" 37 | t.integer "rows_sent", limit: 8 38 | t.integer "rows_examined", limit: 8 39 | t.text "sql" 40 | t.string "explain", limit: 255, default: "none", null: false 41 | t.integer "created_at", null: false 42 | t.integer "updated_at", null: false 43 | end 44 | 45 | add_index "slow_queries", ["datetime"], name: "slow_queries_datetime_index" 46 | add_index "slow_queries", ["period_per_hour"], name: "slow_queries_period_per_hour_index" 47 | add_index "slow_queries", ["period_per_day"], name: "slow_queries_period_per_day_index" 48 | -------------------------------------------------------------------------------- /public/css/prettify-tommorow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | .prettyprint { 4 | background: white; 5 | font-family: Menlo, 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Consolas, monospace; 6 | font-size: 12px; 7 | line-height: 1.5; 8 | border: 1px solid #ccc; 9 | padding: 10px; 10 | } 11 | 12 | .pln { 13 | color: #4d4d4c; 14 | } 15 | 16 | @media screen { 17 | .str { 18 | color: #718c00; 19 | } 20 | 21 | .kwd { 22 | color: #8959a8; 23 | } 24 | 25 | .com { 26 | color: #8e908c; 27 | } 28 | 29 | .typ { 30 | color: #4271ae; 31 | } 32 | 33 | .lit { 34 | color: #f5871f; 35 | } 36 | 37 | .pun { 38 | color: #4d4d4c; 39 | } 40 | 41 | .opn { 42 | color: #4d4d4c; 43 | } 44 | 45 | .clo { 46 | color: #4d4d4c; 47 | } 48 | 49 | .tag { 50 | color: #c82829; 51 | } 52 | 53 | .atn { 54 | color: #f5871f; 55 | } 56 | 57 | .atv { 58 | color: #3e999f; 59 | } 60 | 61 | .dec { 62 | color: #f5871f; 63 | } 64 | 65 | .var { 66 | color: #c82829; 67 | } 68 | 69 | .fun { 70 | color: #4271ae; 71 | } 72 | } 73 | @media print, projection { 74 | .str { 75 | color: #006600; 76 | } 77 | 78 | .kwd { 79 | color: #006; 80 | font-weight: bold; 81 | } 82 | 83 | .com { 84 | color: #600; 85 | font-style: italic; 86 | } 87 | 88 | .typ { 89 | color: #404; 90 | font-weight: bold; 91 | } 92 | 93 | .lit { 94 | color: #004444; 95 | } 96 | 97 | .pun, .opn, .clo { 98 | color: #444400; 99 | } 100 | 101 | .tag { 102 | color: #006; 103 | font-weight: bold; 104 | } 105 | 106 | .atn { 107 | color: #440044; 108 | } 109 | 110 | .atv { 111 | color: #006600; 112 | } 113 | } 114 | /* Specify class=linenums on a pre to get line numbering */ 115 | ol.linenums { 116 | margin-top: 0; 117 | margin-bottom: 0; 118 | } 119 | 120 | /* IE indents via margin-left */ 121 | li.L0, 122 | li.L1, 123 | li.L2, 124 | li.L3, 125 | li.L4, 126 | li.L5, 127 | li.L6, 128 | li.L7, 129 | li.L8, 130 | li.L9 { 131 | /* */ 132 | } 133 | 134 | /* Alternate shading for lines */ 135 | li.L1, 136 | li.L3, 137 | li.L5, 138 | li.L7, 139 | li.L9 { 140 | /* */ 141 | } 142 | -------------------------------------------------------------------------------- /lib/nata2/helpers.rb: -------------------------------------------------------------------------------- 1 | require 'nata2/config' 2 | require 'focuslight-validator' 3 | 4 | module Nata2::Helpers 5 | def validate(*args) 6 | Focuslight::Validator.validate(*args) 7 | end 8 | 9 | def rule(*args) 10 | Focuslight::Validator.rule(*args) 11 | end 12 | 13 | def data 14 | @data ||= Nata2::Data.new 15 | end 16 | 17 | def config(name) 18 | Nata2::Config.get(name) 19 | end 20 | 21 | def labels(service_name, host_name, database_name) 22 | bundles = data.find_bundles(service_name: service_name, host_name: host_name, database_name: database_name) 23 | labels = {} 24 | bundles.each do |bundle| 25 | name = %Q{#{bundle[:database_name]}(#{bundle[:host_name]})} 26 | labels[name] = { color: bundle[:color], path: %Q{/view/#{bundle[:service_name]}/#{bundle[:host_name]}/#{bundle[:database_name]}} } 27 | end 28 | labels 29 | end 30 | 31 | def from_datetime(time_range) 32 | now = Time.now.to_i 33 | case time_range 34 | when 'd' then now - 86400 35 | when 'w' then now - 86400 * 7 36 | when 'm' then now - 86400 * 30 37 | when 'y' then now - 86400 * 365 38 | else 39 | halt 40 | end 41 | end 42 | 43 | def get_graph_data(service_name, host_name, database_name, time_range) 44 | from = from_datetime(time_range) 45 | graph_data = data.get_slow_queries_count_by_period( 46 | per_day: time_range == 'y', from_datetime: from, service_name: service_name, host_name: host_name, database_name: database_name 47 | ) 48 | return [] if graph_data.empty? 49 | 50 | #{"service_name":"service_name","host_name":"host_name2","database_name":"database_name","period":1431082800,"count":4} 51 | 52 | period_column_name, fmt_strftime, plot_per = if time_range == 'y' 53 | [ :period_per_day, '%Y-%m-%d', 3600 * 24 ] 54 | else 55 | [ :period_per_hour, '%Y-%m-%d %H:00', 3600 ] 56 | end 57 | 58 | graph_data = graph_data.to_a.group_by { |gd| gd[period_column_name] } 59 | temp = graph_data.max_by { |_, gd| gd.size } 60 | template = {} 61 | temp.last.each { |tmp| 62 | template.merge!({ 63 | %Q{#{tmp[:database_name]}(#{tmp[:host_name]})}.to_sym => 0 64 | }) 65 | } 66 | result = [] 67 | period = graph_data.min_by { |prd, _| prd }.first 68 | max_period = graph_data.max_by { |prd, _| prd }.first 69 | while period <= max_period do 70 | tgd = template.merge(period: Time.at(period).strftime(fmt_strftime)) 71 | graph_data[period].each do |gd| 72 | tgd = tgd.merge({ %Q{#{gd[:database_name]}(#{gd[:host_name]})}.to_sym => gd[:count] }) 73 | end 74 | period += plot_per 75 | result << tgd 76 | end 77 | return result 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/nata2/mysqldumpslow.rb: -------------------------------------------------------------------------------- 1 | require 'nata2' 2 | 3 | module Nata2::Mysqldumpslow 4 | def self.dump(slow_queries, sort_order = 'c') 5 | summation = {} 6 | slow_queries.each do |slow_query| 7 | sql = slow_query[:sql] 8 | next unless sql 9 | normarized_sql = normalize(sql) 10 | summation = sum(summation, normarized_sql, slow_query) 11 | end 12 | 13 | summarized = summarize(summation) 14 | sort_summarized(summarized, sort_order) 15 | end 16 | 17 | private 18 | 19 | def self.normalize(sql) 20 | sql = sql.gsub(/\b\d+\b/, 'N') 21 | sql = sql.gsub(/\b0x[0-9A-Fa-f]+\b/, 'N') 22 | sql = sql.gsub(/''/, %q{'S'}) 23 | sql = sql.gsub(/''/, %q{'S'}) 24 | sql = sql.gsub(/(\\')/, '') 25 | sql = sql.gsub(/(\\')/, '') 26 | sql = sql.gsub(/'[^']+'/, %q{'S'}) 27 | sql = sql.gsub(/'[^']+'/, %q{'S'}) 28 | # abbreviate massive "in (...)" statements and similar 29 | # s!(([NS],){100,})!sprintf("$2,{repeated %d times}",length($1)/2)!eg; 30 | sql 31 | end 32 | 33 | def self.sum(summation, normarized_sql, slow_query) 34 | summation[normarized_sql] ||= { 35 | count: 0, user: [slow_query[:user]], host: [slow_query[:host]], 36 | query_time: 0.0, lock_time: 0.0, 37 | rows_sent: 0, rows_examined: 0, 38 | raw_sql: slow_query[:sql] 39 | } 40 | 41 | summation[normarized_sql][:count] += 1 42 | summation[normarized_sql][:user] << slow_query[:user] 43 | summation[normarized_sql][:host] << slow_query[:host] 44 | summation[normarized_sql][:query_time] += slow_query[:query_time] 45 | summation[normarized_sql][:lock_time] += slow_query[:lock_time] 46 | summation[normarized_sql][:rows_sent] += slow_query[:rows_sent] 47 | summation[normarized_sql][:rows_examined] += slow_query[:rows_examined] 48 | 49 | summation 50 | end 51 | 52 | def self.summarize(summation) 53 | summation.map do |normarized_sql, c| 54 | count = c[:count].to_f 55 | { 56 | count: count.to_i, user: c[:user].uniq, host: c[:host].uniq, 57 | average: { 58 | query_time: c[:query_time]/count, lock_time: c[:lock_time]/count, 59 | rows_sent: c[:rows_sent]/count, rows_examined: c[:rows_examined]/count 60 | }, 61 | summation: { 62 | query_time: c[:query_time], lock_time: c[:lock_time], 63 | rows_sent: c[:rows_sent], rows_examined: c[:rows_examined] 64 | }, 65 | normarized_sql: normarized_sql, 66 | raw_sql: c[:row_sql] 67 | } 68 | end 69 | end 70 | 71 | def self.sort_summarized(summarized, order) 72 | result = case order 73 | when 'at' 74 | summarized.sort_by { |query| query[:average][:query_time] } 75 | when 'al' 76 | summarized.sort_by { |query| query[:average][:lock_time] } 77 | when 'ar' 78 | summarized.sort_by { |query| query[:average][:rows_sent] } 79 | when 'c' 80 | summarized.sort_by { |query| query[:count] } 81 | when 't' 82 | summarized.sort_by { |query| query[:summation][:query_time] } 83 | when 'l' 84 | summarized.sort_by { |query| query[:summation][:lock_time] } 85 | when 'r' 86 | summarized.sort_by { |query| query[:summation][:rows_sent] } 87 | else 88 | raise ArgumentError, %q{sort order is either of 'at', 'al', 'ar', 't', 'l', 'r' or 'c'.} 89 | end 90 | 91 | result.reverse 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nata2 [![Build Status](https://travis-ci.org/studio3104/nata2.svg)](https://travis-ci.org/studio3104/nata2) 2 | 3 | Nata2 is a tool that can be summarized by integrating the slow query log. 4 | require **Ruby 2.0 or later**. 5 | 6 | ## Usage 7 | 8 | #### Install 9 | 10 | git clone, 11 | 12 | ``` 13 | $ git clone https://github.com/studio3104/nata2.git 14 | ``` 15 | 16 | and bundle install. 17 | 18 | ``` 19 | $ cd nata2 20 | $ bundle install 21 | ``` 22 | 23 | #### Configurations 24 | 25 | describe the setting in `config.toml`. 26 | specify `dburl` of [Sequel](http://sequel.jeremyevans.net/). 27 | 28 | ``` 29 | dburl = "mysql2://YOUR_SPECIFIED_USERNAME:YOUR_SPECIFIED_USER's_PASSWORD@YOUR_MySQL_HOST/nata2" 30 | ``` 31 | 32 | **strongly recommend using `mysql2`.** 33 | because do not have enough test in other databases. 34 | 35 | #### Initialize database 36 | 37 | create `nata2` database. 38 | 39 | ``` 40 | $ mysql -uroot -p -e'CREATE DATABASE `nata2`' 41 | ``` 42 | 43 | create schema with [Ridgepole](https://github.com/winebarrel/ridgepole). 44 | 45 | ``` 46 | $ bundle exec ridgepole -c '{ adapter: mysql2, database: nata2, username: YOUR_SPECIFIED_USERNAME, password: YOUR_SPECIFIED_USER's_PASSWORD, host: YOUR_MySQL_HOST }' --apply 47 | ``` 48 | 49 | #### Launch 50 | 51 | ``` 52 | $ bundle exec rackup 53 | ``` 54 | 55 | ## Register a slow query 56 | 57 | Post parsed slow query log. 58 | 59 | ``` 60 | http://nata2.server/api/1/:service_name/:host_name/:database_name 61 | 62 | { 63 | datetime: 1390883951, 64 | user: 'user', 65 | host: 'localhost', 66 | query_time: 2.001227, 67 | lock_time: 0.0, 68 | rows_sent: 1, 69 | rows_examined:0, 70 | sql: 'SELECT SLEEP(2)' 71 | } 72 | ``` 73 | 74 | If using curl, you should create post request like this. 75 | Header needs `Content-Type: application/x-www-form-urlencoded` (-d use this Content-Type) 76 | Form Fotmat is `key1=value1&key2=value2...` 77 | 78 | ``` 79 | curl -d "datetime=1390883951" \ 80 | -d "user=aa" \ 81 | -d "host=localhost" \ 82 | -d "query_time=2.001227" \ 83 | -d "lock_time=0" \ 84 | -d "rows_sent=1" \ 85 | -d "rows_examined=0" \ 86 | -d "sql=SELECTSLEEP(2)" \ 87 | http://nata2.server/api/1/:service_name/:host_name/:database_name 88 | ``` 89 | 90 | #### Clients 91 | 92 | - [nata2-client](https://github.com/studio3104/nata2-client) 93 | - [fluent-plugin-nata2](https://github.com/studio3104/fluent-plugin-nata2) 94 | 95 | ## Contributing 96 | 97 | 1. Fork it ( http://github.com/studio3104/nata2/fork ) 98 | 2. Create your feature branch (`git checkout -b my-new-feature`) 99 | 3. Commit your changes (`git commit -am 'Add some feature'`) 100 | 4. Push to the branch (`git push origin my-new-feature`) 101 | 5. Create new Pull Request 102 | 103 | ## License 104 | 105 | Copyright (c) 2014 studio3104 106 | 107 | MIT License 108 | 109 | Permission is hereby granted, free of charge, to any person obtaining 110 | a copy of this software and associated documentation files (the 111 | "Software"), to deal in the Software without restriction, including 112 | without limitation the rights to use, copy, modify, merge, publish, 113 | distribute, sublicense, and/or sell copies of the Software, and to 114 | permit persons to whom the Software is furnished to do so, subject to 115 | the following conditions: 116 | 117 | The above copyright notice and this permission notice shall be 118 | included in all copies or substantial portions of the Software. 119 | 120 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 121 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 122 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 123 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 124 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 125 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 126 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 127 | 128 | -------------------------------------------------------------------------------- /spec/nata2/server_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Nata Server Controller' do 4 | let(:service_name) { TestData::ServiceName } 5 | let(:host_name) { TestData::HostName } 6 | let(:database_name) { TestData::DatabaseName } 7 | 8 | describe 'APIs' do 9 | it 'API document' do 10 | get '/docs/api' 11 | expect(last_response).to be_ok 12 | end 13 | 14 | describe 'POST /api/1/:service_name/:host_name/:database_name' do 15 | let(:post_data) { TestData::ParsedSlowQuery } 16 | 17 | it 'create a new slow query record' do 18 | post %Q{/api/1/#{service_name}/#{host_name}/#{database_name}}, post_data 19 | expect(last_response).to be_ok 20 | expect(JSON.parse(last_response.body)['error']).to eq(0) 21 | end 22 | 23 | it 'create a new slow query record without required params' do 24 | post_data_without_required_params = post_data.clone 25 | post_data_without_required_params.delete(:sql) 26 | post %Q{/api/1/#{service_name}/#{host_name}/#{database_name}}, post_data_without_required_params 27 | expect(last_response.status).to eq(400) 28 | expect(JSON.parse(last_response.body, symbolize_names: true)).to eq(error: 1, messages: { sql: 'sql: missing or blank' }) 29 | end 30 | end 31 | end 32 | 33 | it 'top page' do 34 | get '/' 35 | expect(last_response).to be_ok 36 | end 37 | 38 | context 'a slow query view' do 39 | it '200' do 40 | get '/slow_query/1' 41 | expect(last_response).to be_ok 42 | end 43 | it '404' do 44 | get '/slow_query/18446744073709551616' 45 | expect(last_response).to be_not_found 46 | end 47 | end 48 | 49 | context 'a dumped slow query view' do 50 | it '200' do 51 | get '/dumped_query/eyJjb3VudCI6MiwidXNlciI6WyJ1c2VyIl0sImhvc3QiOlsibG9jYWxob3N0Il0sImF2ZXJhZ2UiOnsicXVlcnlfdGltZSI6Mi4wMDEyMjcsImxvY2tfdGltZSI6MC4wLCJyb3dzX3NlbnQiOjEuMCwicm93c19leGFtaW5lZCI6MC4wfSwic3VtbWF0aW9uIjp7InF1ZXJ5X3RpbWUiOjQuMDAyNDU0LCJsb2NrX3RpbWUiOjAuMCwicm93c19zZW50IjoyLCJyb3dzX2V4YW1pbmVkIjowfSwibm9ybWFyaXplZF9zcWwiOiJzZWxlY3Qgc2xlZXAoTikiLCJyYXdfc3FsIjpudWxsfQ==' 52 | expect(last_response).to be_ok 53 | end 54 | it '404' do 55 | get '/dumped_query/not_found' 56 | expect(last_response).to be_not_found 57 | end 58 | end 59 | 60 | context 'view per database' do 61 | it '200' do 62 | get %Q{/view/#{service_name}/#{host_name}/#{database_name}} 63 | expect(last_response).to be_ok 64 | end 65 | it '404' do 66 | get %Q{/view/NOT_REGISTERED_SERVICE/NOT_REGISTERED_HOST/NOT_REGISTERED_DATABASE} 67 | expect(last_response).to be_not_found 68 | end 69 | end 70 | 71 | context 'complex view per database' do 72 | it '200' do 73 | get %Q{/view_complex/#{service_name}/#{database_name}} 74 | expect(last_response).to be_ok 75 | end 76 | it '404' do 77 | get %Q{/view_complex/NOT_REGISTERED_SERVICE/NOT_REGISTERED_DATABASE} 78 | expect(last_response).to be_not_found 79 | end 80 | end 81 | 82 | context 'dump view per database' do 83 | it '200' do 84 | get %Q{/dump/#{service_name}/#{host_name}/#{database_name}} 85 | expect(last_response).to be_ok 86 | end 87 | it '404' do 88 | get %Q{/dump/NOT_REGISTERED_SERVICE/NOT_REGISTERED_HOST/NOT_REGISTERED_DATABASE} 89 | expect(last_response).to be_not_found 90 | end 91 | end 92 | 93 | context 'complex dump view per database' do 94 | it '200' do 95 | get %Q{/dump_complex/#{service_name}/#{database_name}} 96 | expect(last_response).to be_ok 97 | end 98 | it '404' do 99 | get %Q{/dump_complex/NOT_REGISTERED_SERVICE/NOT_REGISTERED_DATABASE} 100 | expect(last_response).to be_not_found 101 | end 102 | end 103 | 104 | context 'list view per database' do 105 | it '200' do 106 | get %Q{/list/#{service_name}/#{host_name}/#{database_name}} 107 | expect(last_response).to be_ok 108 | end 109 | it '404' do 110 | get %Q{/list/NOT_REGISTERED_SERVICE/NOT_REGISTERED_HOST/NOT_REGISTERED_DATABASE} 111 | expect(last_response).to be_not_found 112 | end 113 | end 114 | 115 | context 'complex list view per database' do 116 | it '200' do 117 | get %Q{/list_complex/#{service_name}/#{database_name}} 118 | expect(last_response).to be_ok 119 | end 120 | it '404' do 121 | get %Q{/list_complex/NOT_REGISTERED_SERVICE/NOT_REGISTERED_DATABASE} 122 | expect(last_response).to be_not_found 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /views/view.slim: -------------------------------------------------------------------------------- 1 | h3 2 | span #{@service_name} 3 | - if @host_name 4 | small 5 | span    6 | span.glyphicon.glyphicon-hand-right 7 | span    8 | span #{@host_name} 9 | - if @database_name 10 | small 11 | span    12 | span.glyphicon.glyphicon-hand-right 13 | span    14 | span #{@database_name} 15 | 16 | span#periodSwitch style="float: right;" 17 | .btn-group.btn-group-xs 18 | - if @time_range == 'd' 19 | button.btn.btn-primary.active disabled="" type="button" 24時間 20 | - else 21 | a.btn.btn-default disabled="" href="?t=d" 24時間 22 | - if @time_range == 'w' 23 | button.btn.btn-primary.active disabled="" type="button" 1週間 24 | - else 25 | a.btn.btn-default disabled="" href="?t=w" 1週間 26 | - if @time_range == 'm' 27 | button.btn.btn-primary.active disabled="" type="button" 1ヶ月 28 | - else 29 | a.btn.btn-default disabled="" href="?t=m" 1ヶ月 30 | - if @time_range == 'y' 31 | button.btn.btn-primary.active disabled="" type="button" 1年 32 | - else 33 | a.btn.btn-default disabled="" href="?t=y" 1年 34 | 35 | - list_path = @path.sub(/^\/view/, '/list') 36 | - dump_path = @path.sub(/^\/view/, '/dump') 37 | div#switch align="right" 38 | .btn-group.btn-group-xs 39 | button.btn.btn-default#switchlist disabled="" type="button" onclick="loadSlowQueries('list', '#{list_path}', '?#{URI.encode_www_form(@params.reject { |k, _| %w[ sort ].include?(k) })}')" 40 | span.glyphicon.glyphicon-list 41 | b 履歴 42 | .btn-group.btn-group-xs 43 | - button_params = @params.reject { |k, _| %w[ page sort ].include?(k) } 44 | button.btn.btn-default#switchdumpc disabled="" type="button" onclick="loadSlowQueries('dumpc', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 'c'))}')" 45 | span.glyphicon.glyphicon-plus 46 | b 合計回数順 47 | button.btn.btn-default#switchdumpt disabled="" type="button" onclick="loadSlowQueries('dumpt', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 't'))}')" 48 | span.glyphicon.glyphicon-time 49 | b 合計クエリ実行時間順 50 | button.btn.btn-default#switchdumpl disabled="" type="button" onclick="loadSlowQueries('dumpl', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 'l'))}')" 51 | span.glyphicon.glyphicon-lock 52 | b 合計ロック時間順 53 | button.btn.btn-default#switchdumpr disabled="" type="button" onclick="loadSlowQueries('dumpr', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 'r'))}')" 54 | span.glyphicon.glyphicon-send 55 | b 合計フェッチ行数順 56 | .btn-group.btn-group-xs 57 | button.btn.btn-default#switchdumpat disabled="" type="button" onclick="loadSlowQueries('dumpat', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 'at'))}')" 58 | span.glyphicon.glyphicon-time 59 | b 平均クエリ実行時間順 60 | button.btn.btn-default#switchdumpal disabled="" type="button" onclick="loadSlowQueries('dumpal', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 'al'))}')" 61 | span.glyphicon.glyphicon-lock 62 | b 平均ロック時間順 63 | button.btn.btn-default#switchdumpar disabled="" type="button" onclick="loadSlowQueries('dumpar', '#{dump_path}', '?#{URI.encode_www_form(button_params.merge(sort: 'ar'))}')" 64 | span.glyphicon.glyphicon-send 65 | b 平均フェッチ行数順 66 | 67 | h6 68 | - @labels.each do |name, meta| 69 | a href="#{meta[:path]}" 70 | span.label style="background-color : #{meta[:color]}" #{name} 71 | 72 | div id="count_slow_queries" style="height: 200px;" 73 | 74 | #slowQueries 75 | 76 | javascript: 77 | function loadSlowQueries(id_num, url, params) { 78 | getSlowQueries(id_num, url, params); 79 | window.history.pushState('', '', params); 80 | } 81 | 82 | function getSlowQueries(id, url, params) { 83 | $.ajax({ 84 | beforeSend: function(){ 85 | $("#switch button").attr("disabled","true"); 86 | $("#periodSwitch button").attr("disabled","true"); 87 | $("#periodSwitch a").attr("disabled","true"); 88 | }, 89 | url: url + params, 90 | success: function(result) { 91 | $("#slowQueries").html(result); 92 | }, 93 | complete: function(xhr, event) { 94 | $("#switch button").removeAttr("disabled"); 95 | $("#periodSwitch button").removeAttr("disabled"); 96 | $("#periodSwitch a").removeAttr("disabled"); 97 | $("#switch button").addClass("btn-default") 98 | $("#switch button").removeClass("btn-primary active") 99 | $("#switch" + id).addClass("btn-primary active"); 100 | $("#switch" + id).removeClass("btn-default"); 101 | } 102 | }); 103 | } 104 | 105 | function drawGraph(id, data, labels, colors) { 106 | new Morris.Area({ 107 | element: id, 108 | data: data, 109 | xkey: 'period', 110 | ykeys: labels, 111 | labels: labels, 112 | lineColors: colors, 113 | pointSize: 0, 114 | hideHover: true 115 | }); 116 | } 117 | 118 | window.onload = function() { 119 | loadSlowQueries('#{@root}#{@params['sort']}', '#{@path.sub(/^\/view/, '/' + @root)}', '?#{URI.encode_www_form(@params)}'); 120 | graph_data = eval('#{{JSON.generate(@graph_data)}}'); 121 | console.log(graph_data); 122 | if (graph_data.length === 0) { 123 | $('#count_slow_queries').html("NO DATA"); 124 | } else { 125 | drawGraph('count_slow_queries', graph_data, eval('#{{JSON.generate(@labels.keys)}}'), eval('#{{JSON.generate(@labels.values.map { |l| l[:color] })}}')); 126 | }; 127 | } 128 | 129 | function getUrlVars(hoge) { 130 | var vars = {}, hash; 131 | var hashes = hoge.slice(hoge.indexOf('?') + 1).split('&'); 132 | for (var i = 0; i < hashes.length; i++) { 133 | hash = hashes[i].split('='); 134 | vars[hash[0]] = hash[1]; 135 | } 136 | return vars; 137 | } 138 | 139 | window.onpopstate = function(event) { 140 | var id; 141 | var path; 142 | var params = getUrlVars(location.search); 143 | if (typeof params.sort === 'undefined') { 144 | id = 'list'; 145 | path = location.pathname.replace(/^\/view/, '/list'); 146 | } else { 147 | id = 'dump' + params['sort']; 148 | path = location.pathname.replace(/^\/view/, '/dump'); 149 | }; 150 | getSlowQueries(id, path, location.search); 151 | }; 152 | -------------------------------------------------------------------------------- /lib/nata2/data.rb: -------------------------------------------------------------------------------- 1 | require 'nata2' 2 | require 'nata2/config' 3 | require 'nata2/mysqldumpslow' 4 | require 'time' 5 | require 'sequel' 6 | 7 | #DB = Sequel.connect(Nata2::Config.get(:dburl), logger: Nata2.logger) 8 | DB = Sequel.connect(Nata2::Config.get(:dburl))#, logger: Nata2.logger) 9 | 10 | class Nata2::Data 11 | def initialize 12 | @bundles ||= DB.from(:bundles) 13 | @slow_queries ||= DB.from(:slow_queries) 14 | @explains ||= DB.from(:explains) 15 | end 16 | 17 | def find_bundles(service_name: nil, host_name: nil, database_name: nil) 18 | bundles_where = { service_name: service_name, host_name: host_name, database_name: database_name } 19 | bundles_where.delete_if { |k,v| v.nil? } 20 | @bundles.where(bundles_where).order(:service_name, :database_name, :host_name).all 21 | end 22 | 23 | def get_slow_queries( 24 | id: nil, sort_by_date: false, limit: nil, offset: nil, 25 | from_datetime: 0, to_datetime: Time.now.to_i, 26 | service_name: nil, host_name: nil, database_name: nil 27 | ) 28 | bundles_where = { service_name: service_name, host_name: host_name, database_name: database_name } 29 | bundles_where.delete_if { |k,v| v.nil? } 30 | slow_queries_where = id ? { slow_queries__id: id } : {} 31 | 32 | result = if sort_by_date 33 | @bundles.where(bundles_where).left_outer_join( 34 | :slow_queries, bundle_id: :id 35 | ).where( 36 | slow_queries_where 37 | ).where { 38 | (datetime >= from_datetime) & (datetime <= to_datetime) 39 | }.reverse_order( 40 | :datetime 41 | ).limit(limit).offset(offset) 42 | else 43 | @bundles.where(bundles_where).left_outer_join( 44 | :slow_queries, bundle_id: :id 45 | ).where( 46 | slow_queries_where 47 | ).where { 48 | (datetime >= from_datetime) & (datetime <= to_datetime) 49 | }.limit(limit).offset(offset) 50 | end 51 | result.all 52 | end 53 | 54 | # for graph data 55 | def get_slow_queries_count_by_period( 56 | per_day: false, 57 | id: nil, sort_by_date: false, limit: nil, offset: nil, 58 | from_datetime: 0, to_datetime: Time.now.to_i, 59 | service_name: nil, host_name: nil, database_name: nil 60 | ) 61 | bundles_where = { service_name: service_name, host_name: host_name, database_name: database_name } 62 | bundles_where.delete_if { |k,v| v.nil? } 63 | slow_queries_where = id ? { slow_queries__id: id } : {} 64 | 65 | period = per_day ? :slow_queries__period_per_day : :slow_queries__period_per_hour 66 | @bundles.where(bundles_where).left_outer_join( 67 | :slow_queries, bundle_id: :id 68 | ).where( 69 | slow_queries_where 70 | ).where { 71 | (datetime >= from_datetime) & (datetime <= to_datetime) 72 | }.select_group( 73 | :bundles__service_name, 74 | :bundles__host_name, 75 | :bundles__database_name, 76 | period 77 | ).select_append { 78 | count(period).as(:count) 79 | }.reverse_order( 80 | :datetime 81 | ).limit(limit).offset(offset) 82 | end 83 | 84 | def get_summarized_slow_queries(sort_order, slow_queries) 85 | Nata2::Mysqldumpslow.dump(slow_queries, sort_order) 86 | end 87 | 88 | def get_explains(type: nil) 89 | end 90 | 91 | def register_slow_query(service_name, host_name, database_name, slow_query) 92 | bundles = find_or_create_bundles(service_name, host_name, database_name) 93 | result = nil 94 | current_time = Time.now.to_i 95 | sql = slow_query[:sql] #!!validation!! 96 | 97 | DB.transaction do 98 | @slow_queries.insert( 99 | bundle_id: bundles[:id], 100 | datetime: slow_query[:datetime], 101 | period_per_hour: Time.parse(Time.at(slow_query[:datetime]).to_s.sub(/:\d\d:\d\d/,':00:00')).to_i, # 2015-05-08 19:33:38 +0900 -> 2015-05-08 19:00:00 +0900 102 | period_per_day: Time.parse(Time.at(slow_query[:datetime]).to_s.sub(/\d\d:\d\d:\d\d/,'00:00:00')).to_i, # 2015-05-08 19:33:38 +0900 -> 2015-05-08 00:00:00 +0900 103 | user: slow_query[:user], host: slow_query[:host], 104 | query_time: slow_query[:query_time], lock_time: slow_query[:lock_time], 105 | rows_sent: slow_query[:rows_sent], rows_examined: slow_query[:rows_examined], 106 | sql: sql, 107 | created_at: current_time, updated_at: current_time 108 | ) 109 | 110 | #!!depending on the transaction isolation. verification required.!! 111 | result = @slow_queries.select(:id).where(bundle_id: bundles[:id]).reverse_order(:id).limit(1).first 112 | end 113 | 114 | result 115 | end 116 | 117 | def register_explain(slow_query_id, explain) 118 | result = nil 119 | current_time = Time.now.to_i 120 | 121 | DB.transaction do 122 | @explains.where(slow_query_id: slow_query_id).delete 123 | explain.each do |e| 124 | @explains.insert( 125 | slow_query_id: slow_query_id, 126 | explain_id: e[:id], select_type: e[:select_type], 127 | table: e[:table], type: e[:type], possible_keys: e[:possible_keys], 128 | key: e[:key], key_len: e[:key_len], ref: e[:ref], rows: e[:rows], extra: e[:extra], 129 | created_at: current_time, updated_at: current_time 130 | ) 131 | end 132 | 133 | @slow_queries.where(id: slow_query_id).update(explain: 'done') 134 | result = @explains.select(:id, :slow_query_id).where(slow_query_id: slow_query_id).all 135 | end 136 | 137 | result 138 | end 139 | 140 | def migrate_data_for_1_0_0 141 | DB.transaction do 142 | @slow_queries.each do |slow_query| 143 | period_per_hour = Time.parse(Time.at(slow_query[:datetime]).to_s.sub(/:\d\d:\d\d/,':00:00')).to_i # 2015-05-08 19:33:38 +0900 -> 2015-05-08 19:00:00 +0900 144 | period_per_day = Time.parse(Time.at(slow_query[:datetime]).to_s.sub(/\d\d:\d\d:\d\d/,'00:00:00')).to_i # 2015-05-08 19:33:38 +0900 -> 2015-05-08 00:00:00 +0900 145 | @slow_queries.where(id: slow_query[:id]).update(period_per_hour: period_per_hour, period_per_day: period_per_day) 146 | end 147 | end 148 | end 149 | 150 | private 151 | 152 | def config(name) 153 | Nata2::Config.get(name) 154 | end 155 | 156 | def find_or_create_bundles(service_name, host_name, database_name) 157 | bundles = find_bundles(service_name: service_name, host_name: host_name, database_name: database_name).first 158 | return bundles if bundles 159 | 160 | DB.transaction do 161 | current_time = Time.now.to_i 162 | color = '#' #create random color code 163 | 6.times { color = color + %w{0 1 2 3 4 5 6 7 8 9 a b c d e f}.shuffle.first } 164 | 165 | @bundles.insert( 166 | service_name: service_name, 167 | host_name: host_name, 168 | database_name: database_name, 169 | color: color, 170 | created_at: current_time, 171 | updated_at: current_time, 172 | ) 173 | 174 | bundles = @bundles.where(service_name: service_name, host_name: host_name, database_name: database_name).first 175 | end 176 | 177 | bundles 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /lib/nata2/server.rb: -------------------------------------------------------------------------------- 1 | require 'nata2' 2 | require 'nata2/data' 3 | require 'nata2/helpers' 4 | require 'json' 5 | require 'base64' 6 | require 'uri' 7 | require 'sinatra/base' 8 | require 'sinatra/json' 9 | require 'slim' 10 | 11 | module Nata2 12 | class Server < Sinatra::Base 13 | configure do 14 | Slim::Engine.default_options[:pretty] = true 15 | app_root = File.dirname(__FILE__) + '/../..' 16 | set :public_folder, app_root + '/public' 17 | set :views, app_root + '/views' 18 | end 19 | 20 | configure :development do 21 | require 'sinatra/reloader' 22 | register Sinatra::Reloader 23 | set :show_exception, false 24 | set :show_exception, :after_handler 25 | end 26 | 27 | SUPPRESS_KEYS_TO_OPTIMIZE = %w[ service_name host_name database_name page amp splat captures ] 28 | helpers do 29 | def optimize_params(_params) 30 | params = _params.dup 31 | SUPPRESS_KEYS_TO_OPTIMIZE.each { |key| params.delete(key) } 32 | params 33 | end 34 | 35 | include Nata2::Helpers 36 | end 37 | 38 | not_found do 39 | '404' 40 | end 41 | 42 | get '/slow_query/:query_id' do 43 | @slow_query = data.get_slow_queries(id: params[:query_id]).first 44 | raise Sinatra::NotFound unless @slow_query 45 | slim :slow_query 46 | end 47 | 48 | get '/dumped_query/:dumped_query_base64encoded' do 49 | begin 50 | @dumped_query = JSON.parse(Base64.decode64(params[:dumped_query_base64encoded]), symbolize_names: true) 51 | rescue JSON::ParserError 52 | raise Sinatra::NotFound 53 | end 54 | slim :dumped_query 55 | end 56 | 57 | get '/view/:service_name/:host_name/:database_name' do 58 | @service_name = params['service_name'] 59 | @host_name = params[:host_name] 60 | @database_name = params[:database_name] 61 | bundles = data.find_bundles(service_name: @service_name, host_name: @host_name, database_name: @database_name) 62 | raise Sinatra::NotFound if bundles.empty? 63 | @time_range = params['t'] || 'w' 64 | @graph_data = get_graph_data(@service_name, @host_name, @database_name, @time_range) 65 | @path = request.path 66 | @labels = labels(@service_name, @host_name, @database_name) 67 | @params = optimize_params(params) 68 | @root = @params.has_key?('sort') ? 'dump' : 'list' 69 | slim :view 70 | end 71 | 72 | get '/view_complex/:service_name/:database_name' do 73 | @service_name = params['service_name'] 74 | @database_name = params[:database_name] 75 | bundles = data.find_bundles(service_name: @service_name, database_name: @database_name) 76 | raise Sinatra::NotFound if bundles.empty? 77 | @path = request.path 78 | @labels = labels(@service_name, @host_name, @database_name) 79 | @time_range = params['t'] || 'w' 80 | @graph_data = get_graph_data(@service_name, @host_name, @database_name, @time_range) 81 | @params = optimize_params(params) 82 | @root = @params.has_key?('sort') ? 'dump' : 'list' 83 | slim :view 84 | end 85 | 86 | get '/dump/:service_name/:host_name/:database_name' do 87 | sort = params['sort'] || 'c' 88 | service_name = params[:service_name] 89 | host_name = params[:host_name] 90 | database_name = params[:database_name] 91 | bundles = data.find_bundles(service_name: service_name, host_name: host_name, database_name: database_name) 92 | raise Sinatra::NotFound if bundles.empty? 93 | from = from_datetime(params['t'] || 'w') 94 | slow_queries = data.get_slow_queries(from_datetime: from, service_name: service_name, host_name: host_name, database_name: database_name) 95 | @slow_queries = data.get_summarized_slow_queries(sort, slow_queries) 96 | slim :dump 97 | end 98 | 99 | get '/dump_complex/:service_name/:database_name' do 100 | sort = params['sort'] || 'c' 101 | service_name = params[:service_name] 102 | database_name = params[:database_name] 103 | bundles = data.find_bundles(service_name: service_name, database_name: database_name) 104 | raise Sinatra::NotFound if bundles.empty? 105 | from = from_datetime(params['t'] || 'w') 106 | slow_queries = data.get_slow_queries(from_datetime: from, service_name: service_name, database_name: database_name) 107 | @slow_queries = data.get_summarized_slow_queries(sort, slow_queries) 108 | slim :dump 109 | end 110 | 111 | get '/list/:service_name/:host_name/:database_name' do 112 | service_name = params[:service_name] 113 | host_name = params[:host_name] 114 | database_name = params[:database_name] 115 | bundles = data.find_bundles(service_name: service_name, host_name: host_name, database_name: database_name) 116 | raise Sinatra::NotFound if bundles.empty? 117 | from = from_datetime(params['t'] || 'w') 118 | @page = params[:page] ? params[:page].to_i : 1 119 | @params = optimize_params(params) 120 | limit = 101 121 | offset = limit * (@page - 1) - 1 122 | offset = offset < 0 ? 0 : offset 123 | 124 | slow_queries = data.get_slow_queries(sort_by_date: true, from_datetime: from, service_name: service_name, host_name: host_name, database_name: database_name, limit: limit, offset: offset) 125 | if slow_queries.size <= 100 126 | @disabled_next = true 127 | else 128 | slow_queries.pop 129 | end 130 | @slow_queries_per_day = {} 131 | slow_queries.each do |slow_query| 132 | day = Time.at(slow_query[:datetime]).strftime('%Y/%m/%d') 133 | @slow_queries_per_day[day] ||= [] 134 | @slow_queries_per_day[day] << slow_query 135 | end 136 | 137 | slim :list 138 | end 139 | 140 | get '/list_complex/:service_name/:database_name' do 141 | service_name = params[:service_name] 142 | database_name = params[:database_name] 143 | bundles = data.find_bundles(service_name: service_name, database_name: database_name) 144 | raise Sinatra::NotFound if bundles.empty? 145 | from = from_datetime(params['t'] || 'w') 146 | @page = params[:page] ? params[:page].to_i : 1 147 | @params = optimize_params(params) 148 | limit = 101 149 | offset = limit * (@page - 1) - 1 150 | offset = offset < 0 ? 0 : offset 151 | 152 | slow_queries = data.get_slow_queries(sort_by_date: true, from_datetime: from, service_name: service_name, database_name: database_name, limit: limit, offset: offset) 153 | if slow_queries.size <= 100 154 | @disabled_next = true 155 | else 156 | slow_queries.pop 157 | end 158 | @slow_queries_per_day = {} 159 | slow_queries.each do |slow_query| 160 | day = Time.at(slow_query[:datetime]).strftime('%Y/%m/%d') 161 | @slow_queries_per_day[day] ||= [] 162 | @slow_queries_per_day[day] << slow_query 163 | end 164 | 165 | slim :list 166 | end 167 | 168 | get '/' do 169 | @bundles = {} 170 | @complex = {} 171 | complex = {} 172 | data.find_bundles.each do |bundle| 173 | service, database = [ bundle[:service_name], bundle[:database_name] ] 174 | @bundles[service] ||= [] 175 | @bundles[service] << { color: bundle[:color], database: database, host: bundle[:host_name] } 176 | @complex[service] ||= [] 177 | next if @complex[service].include?(database) 178 | complex[service] ||= {} 179 | complex[service][database] ||= 0 180 | complex[service][database] += 1 181 | @complex[service] << database if complex[service][database] > 1 182 | end 183 | slim :index 184 | end 185 | 186 | post '/api/1/:service_name/:host_name/:database_name' do 187 | req_params = validate(params, { 188 | service_name: { rule: rule(:not_blank) }, host_name: { rule: rule(:not_blank) }, database_name: { rule: rule(:not_blank) }, 189 | user: { rule: rule(:regexp, /.*/) }, host: { rule: rule(:regexp, /.*/) }, 190 | query_time: { rule: rule(:float) }, lock_time: { rule: rule(:float) }, 191 | rows_sent: { rule: rule(:uint) }, rows_examined: { rule: rule(:uint) }, 192 | sql: { rule: rule(:not_blank) }, datetime: { rule: rule(:natural) } 193 | }) 194 | 195 | if req_params.has_error? 196 | halt 400, JSON.generate(error: 1, messages: req_params.errors) 197 | end 198 | 199 | req_params = req_params.hash 200 | result = data.register_slow_query( 201 | req_params.delete(:service_name), 202 | req_params.delete(:host_name), 203 | req_params.delete(:database_name), 204 | req_params 205 | ) 206 | 207 | result ? JSON.generate(error: 0, data: result) : JSON.generate(error: 1, messages: []) 208 | end 209 | 210 | post '/api/1/explain/:slow_query_id' do 211 | slow_query_id = validate(params, { slow_query_id: { rule: rule(:not_blank) } }) 212 | if slow_query_id.has_error? 213 | halt 400, json({ error: 1, messages: slow_query_id.errors }) 214 | end 215 | slow_query_id = slow_query_id[:slow_query_id] 216 | 217 | post_spec = { 218 | id: { rule: rule(:natural) }, 219 | select_type: { rule: rule(:choice, 220 | 'SIMPLE', 'PRIMARY', 221 | 'UNION', 'UNION RESULT', 'DEPENDENT UNION', 'UNCACHEABLE UNION', # UNION 222 | 'SUBQUERY', 'DEPENDENT SUBQUERY', 'UNCACHEABLE SUBQUERY', 'DERIVED' # SUBQUERY 223 | ) }, 224 | table: { rule: rule(:not_blank) }, 225 | type: { rule: rule(:choice, 'system' ,'const', 'eq_ref', 'ref', 'range', 'index', 'ALL') }, 226 | possible_keys: { default: nil }, key: { default: nil }, key_len: { default: nil }, ref: { default: nil }, 227 | rows: { rule: rule(:uint) }, extra: { default: nil }, 228 | } 229 | 230 | explain = [] 231 | explain_error = false 232 | params[:explain] = JSON.parse(request.body.read) 233 | if !params[:explain].is_a?(Array) 234 | halt 400, json({ error:1, messages: [] }) 235 | end 236 | params[:explain].each do |p| 237 | record = p.symbolize_keys 238 | record = record.delete_if { |k,v| v == 'NULL' } 239 | exp = validate(record, post_spec) 240 | explain_error = true if exp.has_error? 241 | explain << exp 242 | end 243 | 244 | if explain_error 245 | halt 400, json({ error: 1, messages: explain.map { |exp| exp.errors } }) 246 | end 247 | 248 | result = data.register_explain(slow_query_id, explain) 249 | result ? json({ error: 0, data: result }) : json({ error: 1, messages: [] }) 250 | end 251 | 252 | get '/docs/api' do 253 | slim :'docs/api' 254 | end 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /public/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.1.1 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | .btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn:active,.btn.active{background-image:none}.btn-default{background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;text-shadow:0 1px 0 #fff;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-color:#357ebd}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f3f3f3 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f3f3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .navbar-nav>.active>a{background-image:-webkit-linear-gradient(top,#222 0,#282828 100%);background-image:linear-gradient(to bottom,#222 0,#282828 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0)}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0)}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0)}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0)}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0)}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);border-color:#3278b3}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0)}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /public/js/prettify.js: -------------------------------------------------------------------------------- 1 | !function(){var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; 2 | (function(){function S(a){function d(e){var b=e.charCodeAt(0);if(b!==92)return b;var a=e.charAt(1);return(b=r[a])?b:"0"<=a&&a<="7"?parseInt(e.substring(1),8):a==="u"||a==="x"?parseInt(e.substring(2),16):e.charCodeAt(1)}function g(e){if(e<32)return(e<16?"\\x0":"\\x")+e.toString(16);e=String.fromCharCode(e);return e==="\\"||e==="-"||e==="]"||e==="^"?"\\"+e:e}function b(e){var b=e.substring(1,e.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),e=[],a= 3 | b[0]==="^",c=["["];a&&c.push("^");for(var a=a?1:0,f=b.length;a122||(l<65||h>90||e.push([Math.max(65,h)|32,Math.min(l,90)|32]),l<97||h>122||e.push([Math.max(97,h)&-33,Math.min(l,122)&-33]))}}e.sort(function(e,a){return e[0]-a[0]||a[1]-e[1]});b=[];f=[];for(a=0;ah[0]&&(h[1]+1>h[0]&&c.push("-"),c.push(g(h[1])));c.push("]");return c.join("")}function s(e){for(var a=e.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),c=a.length,d=[],f=0,h=0;f=2&&e==="["?a[f]=b(l):e!=="\\"&&(a[f]=l.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return a.join("")}for(var x=0,m=!1,j=!1,k=0,c=a.length;k=5&&"lang-"===w.substring(0,5))&&!(t&&typeof t[1]==="string"))f=!1,w="src";f||(r[z]=w)}h=c;c+=z.length;if(f){f=t[1];var l=z.indexOf(f),B=l+f.length;t[2]&&(B=z.length-t[2].length,l=B-f.length);w=w.substring(5);H(j+h,z.substring(0,l),g,k);H(j+h+l,f,I(w,f),k);H(j+h+B,z.substring(B),g,k)}else k.push(j+h,w)}a.g=k}var b={},s;(function(){for(var g=a.concat(d),j=[],k={},c=0,i=g.length;c=0;)b[n.charAt(e)]=r;r=r[1];n=""+r;k.hasOwnProperty(n)||(j.push(r),k[n]=q)}j.push(/[\S\s]/);s=S(j)})();var x=d.length;return g}function v(a){var d=[],g=[];a.tripleQuotedStrings?d.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?d.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, 10 | q,"'\"`"]):d.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&g.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var b=a.hashComments;b&&(a.cStyleComments?(b>1?d.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):d.push(["com",/^#(?:(?:define|e(?:l|nd)if|else|error|ifn?def|include|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),g.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h(?:h|pp|\+\+)?|[a-z]\w*)>/,q])):d.push(["com", 11 | /^#[^\n\r]*/,q,"#"]));a.cStyleComments&&(g.push(["com",/^\/\/[^\n\r]*/,q]),g.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));if(b=a.regexLiterals){var s=(b=b>1?"":"\n\r")?".":"[\\S\\s]";g.push(["lang-regex",RegExp("^(?:^^\\.?|[+-]|[!=]=?=?|\\#|%=?|&&?=?|\\(|\\*=?|[+\\-]=|->|\\/=?|::?|<>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*("+("/(?=[^/*"+b+"])(?:[^/\\x5B\\x5C"+b+"]|\\x5C"+s+"|\\x5B(?:[^\\x5C\\x5D"+b+"]|\\x5C"+ 12 | s+")*(?:\\x5D|$))+/")+")")])}(b=a.types)&&g.push(["typ",b]);b=(""+a.keywords).replace(/^ | $/g,"");b.length&&g.push(["kwd",RegExp("^(?:"+b.replace(/[\s,]+/g,"|")+")\\b"),q]);d.push(["pln",/^\s+/,q," \r\n\t\u00a0"]);b="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(b+="(?!s*/)");g.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/, 13 | q],["pun",RegExp(b),q]);return C(d,g)}function J(a,d,g){function b(a){var c=a.nodeType;if(c==1&&!x.test(a.className))if("br"===a.nodeName)s(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)b(a);else if((c==3||c==4)&&g){var d=a.nodeValue,i=d.match(m);if(i)c=d.substring(0,i.index),a.nodeValue=c,(d=d.substring(i.index+i[0].length))&&a.parentNode.insertBefore(j.createTextNode(d),a.nextSibling),s(a),c||a.parentNode.removeChild(a)}}function s(a){function b(a,c){var d= 14 | c?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=b(e,1),g=a.nextSibling;e.appendChild(d);for(var i=g;i;i=g)g=i.nextSibling,e.appendChild(i)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),d;(d=a.parentNode)&&d.nodeType===1;)a=d;c.push(a)}for(var x=/(?:^|\s)nocode(?:\s|$)/,m=/\r\n?|\n/,j=a.ownerDocument,k=j.createElement("li");a.firstChild;)k.appendChild(a.firstChild);for(var c=[k],i=0;i=0;){var b=d[g];F.hasOwnProperty(b)?D.console&&console.warn("cannot override language handler %s",b):F[b]=a}}function I(a,d){if(!a||!F.hasOwnProperty(a))a=/^\s*=l&&(b+=2);g>=B&&(r+=2)}}finally{if(f)f.style.display=h}}catch(u){D.console&&console.log(u&&u.stack||u)}}var D=window,y=["break,continue,do,else,for,if,return,while"],E=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],M=[E,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],N=[E,"abstract,assert,boolean,byte,extends,final,finally,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"], 19 | O=[N,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,internal,into,is,let,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var,virtual,where"],E=[E,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],P=[y,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], 20 | Q=[y,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],W=[y,"as,assert,const,copy,drop,enum,extern,fail,false,fn,impl,let,log,loop,match,mod,move,mut,priv,pub,pure,ref,self,static,struct,true,trait,type,unsafe,use"],y=[y,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],R=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)\b/, 21 | V=/\S/,X=v({keywords:[M,O,E,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",P,Q,y],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),F={};p(X,["default-code"]);p(C([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-", 22 | /^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);p(C([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/], 23 | ["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);p(C([],[["atv",/^[\S\s]+/]]),["uq.val"]);p(v({keywords:M,hashComments:!0,cStyleComments:!0,types:R}),["c","cc","cpp","cxx","cyc","m"]);p(v({keywords:"null,true,false"}),["json"]);p(v({keywords:O,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:R}), 24 | ["cs"]);p(v({keywords:N,cStyleComments:!0}),["java"]);p(v({keywords:y,hashComments:!0,multiLineStrings:!0}),["bash","bsh","csh","sh"]);p(v({keywords:P,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py","python"]);p(v({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:2}),["perl","pl","pm"]);p(v({keywords:Q, 25 | hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb","ruby"]);p(v({keywords:E,cStyleComments:!0,regexLiterals:!0}),["javascript","js"]);p(v({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);p(v({keywords:W,cStyleComments:!0,multilineStrings:!0}),["rc","rs","rust"]); 26 | p(C([],[["str",/^[\S\s]+/]]),["regex"]);var Y=D.PR={createSimpleLexer:C,registerLangHandler:p,sourceDecorator:v,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:D.prettyPrintOne=function(a,d,g){var b=document.createElement("div");b.innerHTML="
"+a+"
";b=b.firstChild;g&&J(b,g,!0);K({h:d,j:g,c:b,i:1}); 27 | return b.innerHTML},prettyPrint:D.prettyPrint=function(a,d){function g(){for(var b=D.PR_SHOULD_USE_CONTINUATION?c.now()+250:Infinity;ithis.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});return this.$element.trigger(j),j.isDefaultPrevented()?void 0:(this.sliding=!0,f&&this.pause(),this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")),f&&this.cycle(),this)};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("collapse in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);!e&&f.toggle&&"show"==c&&(c=!c),e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(jQuery),+function(a){"use strict";function b(b){a(d).remove(),a(e).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;(e||"destroy"!=c)&&(e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]())})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(a(c).is("body")?window:c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);{var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})}},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);if(g&&b<=e[0])return g!=(a=f[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parentsUntil(this.options.target,".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=this.pinnedOffset=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(b.RESET).addClass("affix");var a=this.$window.scrollTop(),c=this.$element.offset();return this.pinnedOffset=c.top-a},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"top"==this.affixed&&(e.top+=d),"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top(this.$element)),"function"==typeof h&&(h=f.bottom(this.$element));var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;if(this.affixed!==i){this.unpin&&this.$element.css("top","");var j="affix"+(i?"-"+i:""),k=a.Event(j+".bs.affix");this.$element.trigger(k),k.isDefaultPrevented()||(this.affixed=i,this.unpin="bottom"==i?this.getPinnedOffset():null,this.$element.removeClass(b.RESET).addClass(j).trigger(a.Event(j.replace("affix","affixed"))),"bottom"==i&&this.$element.offset({top:c-h-this.$element.height()}))}}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(jQuery); -------------------------------------------------------------------------------- /public/js/morris.min.js: -------------------------------------------------------------------------------- 1 | (function(){var e,t,n,r,i=[].slice,s={}.hasOwnProperty,o=function(e,t){function r(){this.constructor=e}for(var n in t)s.call(t,n)&&(e[n]=t[n]);return r.prototype=t.prototype,e.prototype=new r,e.__super__=t.prototype,e},u=function(e,t){return function(){return e.apply(t,arguments)}},a=[].indexOf||function(e){for(var t=0,n=this.length;tn.length&&(r+=i.slice(n.length)),r):"-"},t.pad2=function(e){return(e<10?"0":"")+e},t.Grid=function(n){function r(t){var n=this;typeof t.element=="string"?this.el=e(document.getElementById(t.element)):this.el=e(t.element);if(this.el==null||this.el.length===0)throw new Error("Graph container element not found");this.el.css("position")==="static"&&this.el.css("position","relative"),this.options=e.extend({},this.gridDefaults,this.defaults||{},t),typeof this.options.units=="string"&&(this.options.postUnits=t.units),this.raphael=new Raphael(this.el[0]),this.elementWidth=null,this.elementHeight=null,this.dirty=!1,this.init&&this.init(),this.setData(this.options.data),this.el.bind("mousemove",function(e){var t;return t=n.el.offset(),n.fire("hovermove",e.pageX-t.left,e.pageY-t.top)}),this.el.bind("mouseout",function(e){return n.fire("hoverout")}),this.el.bind("touchstart touchmove touchend",function(e){var t,r;return r=e.originalEvent.touches[0]||e.originalEvent.changedTouches[0],t=n.el.offset(),n.fire("hover",r.pageX-t.left,r.pageY-t.top),r}),this.el.bind("click",function(e){var t;return t=n.el.offset(),n.fire("gridclick",e.pageX-t.left,e.pageY-t.top)}),this.postInit&&this.postInit()}return o(r,n),r.prototype.gridDefaults={dateFormat:null,axes:!0,grid:!0,gridLineColor:"#aaa",gridStrokeWidth:.5,gridTextColor:"#888",gridTextSize:12,gridTextFamily:"sans-serif",gridTextWeight:"normal",hideHover:!1,yLabelFormat:null,xLabelAngle:0,numLines:5,padding:25,parseTime:!0,postUnits:"",preUnits:"",ymax:"auto",ymin:"auto 0",goals:[],goalStrokeWidth:1,goalLineColors:["#666633","#999966","#cc6666","#663333"],events:[],eventStrokeWidth:1,eventLineColors:["#005a04","#ccffbb","#3a5f0b","#005502"]},r.prototype.setData=function(e,n){var r,i,s,o,u,a,f,l,c,h,p,d,v,m;n==null&&(n=!0),this.options.data=e;if(e==null||e.length===0){this.data=[],this.raphael.clear(),this.hover!=null&&this.hover.hide();return}d=this.cumulative?0:null,v=this.cumulative?0:null,this.options.goals.length>0&&(u=Math.min.apply(null,this.options.goals),o=Math.max.apply(null,this.options.goals),v=v!=null?Math.min(v,u):u,d=d!=null?Math.max(d,o):o),this.data=function(){var n,r,o;o=[];for(s=n=0,r=e.length;nt.x)-(t.x>e.x)})),this.xmin=this.data[0].x,this.xmax=this.data[this.data.length-1].x,this.events=[],this.options.parseTime&&this.options.events.length>0&&(this.events=function(){var e,n,i,s;i=this.options.events,s=[];for(e=0,n=i.length;e=n;h=e+=l)r.push(h);return r}.call(this));this.dirty=!0;if(n)return this.redraw()},r.prototype.yboundary=function(e,t){var n,r;return n=this.options["y"+e],typeof n=="string"?n.slice(0,4)==="auto"?n.length>5?(r=parseInt(n.slice(5),10),t==null?r:Math[e](t,r)):t!=null?t:0:parseInt(n,10):n},r.prototype.autoGridLines=function(e,t,n){var r,i,s,o,u,a,f,l,c;return u=t-e,c=Math.floor(Math.log(u)/Math.log(10)),f=Math.pow(10,c),i=Math.floor(e/f)*f,r=Math.ceil(t/f)*f,a=(r-i)/(n-1),f===1&&a>1&&Math.ceil(a)!==a&&(a=Math.ceil(a),r=i+a*(n-1)),i<0&&r>0&&(i=Math.floor(e/a)*a,r=Math.ceil(t/a)*a),a<1?(o=Math.floor(Math.log(a)/Math.log(10)),s=function(){var e,t;t=[];for(l=e=i;i<=r?e<=r:e>=r;l=e+=a)t.push(parseFloat(l.toFixed(1-o)));return t}()):s=function(){var e,t;t=[];for(l=e=i;i<=r?e<=r:e>=r;l=e+=a)t.push(l);return t}(),s},r.prototype._calc=function(){var e,t,n,r,i,s;i=this.el.width(),n=this.el.height();if(this.elementWidth!==i||this.elementHeight!==n||this.dirty){this.elementWidth=i,this.elementHeight=n,this.dirty=!1,this.left=this.options.padding,this.right=this.elementWidth-this.options.padding,this.top=this.options.padding,this.bottom=this.elementHeight-this.options.padding,this.options.axes&&(s=function(){var e,n,r,i;r=this.grid,i=[];for(e=0,n=r.length;et;r=0<=t?++e:--e)n.push(this.measureText(this.data[r].text,-this.options.xLabelAngle).height);return n}.call(this),this.bottom-=Math.max.apply(Math,e)),this.width=Math.max(1,this.right-this.left),this.height=Math.max(1,this.bottom-this.top),this.dx=this.width/(this.xmax-this.xmin),this.dy=this.height/(this.ymax-this.ymin);if(this.calc)return this.calc()}},r.prototype.transY=function(e){return this.bottom-(e-this.ymin)*this.dy},r.prototype.transX=function(e){return this.data.length===1?(this.left+this.right)/2:this.left+(e-this.xmin)*this.dx},r.prototype.redraw=function(){this.raphael.clear(),this._calc(),this.drawGrid(),this.drawGoals(),this.drawEvents();if(this.draw)return this.draw()},r.prototype.measureText=function(e,t){var n,r;return t==null&&(t=0),r=this.raphael.text(100,100,e).attr("font-size",this.options.gridTextSize).attr("font-family",this.options.gridTextFamily).attr("font-weight",this.options.gridTextWeight).rotate(t),n=r.getBBox(),r.remove(),n},r.prototype.yAxisFormat=function(e){return this.yLabelFormat(e)},r.prototype.yLabelFormat=function(e){return typeof this.options.yLabelFormat=="function"?this.options.yLabelFormat(e):""+this.options.preUnits+t.commas(e)+this.options.postUnits},r.prototype.updateHover=function(e,t){var n,r;n=this.hitTest(e,t);if(n!=null)return(r=this.hover).update.apply(r,n)},r.prototype.drawGrid=function(){var e,t,n,r,i,s;if(this.options.grid===!1&&this.options.axes===!1)return;i=this.grid,s=[];for(n=0,r=i.length;n"),this.el.hide(),this.options.parent.append(this.el)}return n.defaults={"class":"morris-hover morris-default-style"},n.prototype.update=function(e,t,n){return this.html(e),this.show(),this.moveTo(t,n)},n.prototype.html=function(e){return this.el.html(e)},n.prototype.moveTo=function(e,t){var n,r,i,s,o,u;return o=this.options.parent.innerWidth(),s=this.options.parent.innerHeight(),r=this.el.outerWidth(),n=this.el.outerHeight(),i=Math.min(Math.max(0,e-r/2),o-r),t!=null?(u=t-n-10,u<0&&(u=t+10,u+n>s&&(u=s/2-n/2))):u=s/2-n/2,this.el.css({left:i+"px",top:parseInt(u)+"px"})},n.prototype.show=function(){return this.el.show()},n.prototype.hide=function(){return this.el.hide()},n}(),t.Line=function(e){function n(e){this.hilight=u(this.hilight,this),this.onHoverOut=u(this.onHoverOut,this),this.onHoverMove=u(this.onHoverMove,this),this.onGridClick=u(this.onGridClick,this);if(!(this instanceof t.Line))return new t.Line(e);n.__super__.constructor.call(this,e)}return o(n,e),n.prototype.init=function(){this.pointGrow=Raphael.animation({r:this.options.pointSize+3},25,"linear"),this.pointShrink=Raphael.animation({r:this.options.pointSize},25,"linear");if(this.options.hideHover!=="always")return this.hover=new t.Hover({parent:this.el}),this.on("hovermove",this.onHoverMove),this.on("hoverout",this.onHoverOut),this.on("gridclick",this.onGridClick)},n.prototype.defaults={lineWidth:3,pointSize:4,lineColors:["#0b62a4","#7A92A3","#4da74d","#afd8f8","#edc240","#cb4b4b","#9440ed"],pointWidths:[1],pointStrokeColors:["#ffffff"],pointFillColors:[],smooth:!0,xLabels:"auto",xLabelFormat:null,xLabelMargin:24,continuousLine:!0,hideHover:!1},n.prototype.calc=function(){return this.calcPoints(),this.generatePaths()},n.prototype.calcPoints=function(){var e,t,n,r,i,s;i=this.data,s=[];for(n=0,r=i.length;n"+r.label+"",u=r.y;for(n=s=0,o=u.length;s\n "+this.options.labels[n]+":\n "+this.yLabelFormat(i)+"\n";return typeof this.options.hoverCallback=="function"&&(t=this.options.hoverCallback(e,this.options,t)),[t,r._x,r._ymax]},n.prototype.generatePaths=function(){var e,n,r,i,s;return this.paths=function(){var o,u,f,l;l=[];for(r=o=0,u=this.options.ykeys.length;0<=u?ou;r=0<=u?++o:--o)s=this.options.smooth===!0||(f=this.options.ykeys[r],a.call(this.options.smooth,f)>=0),n=function(){var e,t,n,s;n=this.data,s=[];for(e=0,t=n.length;e1?l.push(t.Line.createPath(n,s,this.bottom)):l.push(null);return l}.call(this)},n.prototype.draw=function(){this.options.axes&&this.drawXAxis(),this.drawSeries();if(this.options.hideHover===!1)return this.displayHoverForRow(this.data.length-1)},n.prototype.drawXAxis=function(){var e,n,r,i,s,o,u,a,f,l,c=this;u=this.bottom+this.options.padding/2,s=null,i=null,e=function(e,t){var n,r,o,a,f;return n=c.drawXAxisLabel(c.transX(t),u,e),f=n.getBBox(),n.transform("r"+ -c.options.xLabelAngle),r=n.getBBox(),n.transform("t0,"+r.height/2+"..."),c.options.xLabelAngle!==0&&(a=-0.5*f.width*Math.cos(c.options.xLabelAngle*Math.PI/180),n.transform("t"+a+",0...")),r=n.getBBox(),(s==null||s>=r.x+r.width||i!=null&&i>=r.x)&&r.x>=0&&r.x+r.width=0;e=r<=0?++t:--t)this._drawLineFor(e);s=[];for(e=n=i=this.options.ykeys.length-1;i<=0?n<=0:n>=0;e=i<=0?++n:--n)s.push(this._drawPointFor(e));return s},n.prototype._drawPointFor=function(e){var t,n,r,i,s,o;this.seriesPoints[e]=[],s=this.data,o=[];for(r=0,i=s.length;r=i;t=0<=i?++n:--n)this.seriesPoints[t][this.prevHilight]&&this.seriesPoints[t][this.prevHilight].animate(this.pointShrink);if(e!==null&&this.prevHilight!==e)for(t=r=0,s=this.seriesPoints.length-1;0<=s?r<=s:r>=s;t=0<=s?++r:--r)this.seriesPoints[t][e]&&this.seriesPoints[t][e].animate(this.pointGrow);return this.prevHilight=e},n.prototype.colorFor=function(e,t,n){return typeof this.options.lineColors=="function"?this.options.lineColors.call(this,e,t,n):n==="point"?this.options.pointFillColors[t%this.options.pointFillColors.length]||this.options.lineColors[t%this.options.lineColors.length]:this.options.lineColors[t%this.options.lineColors.length]},n.prototype.drawXAxisLabel=function(e,t,n){return this.raphael.text(e,t,n).attr("font-size",this.options.gridTextSize).attr("font-family",this.options.gridTextFamily).attr("font-weight",this.options.gridTextWeight).attr("fill",this.options.gridTextColor)},n.prototype.drawLinePath=function(e,t){return this.raphael.path(e).attr("stroke",t).attr("stroke-width",this.options.lineWidth)},n.prototype.drawLinePoint=function(e,t,n,r,i){return this.raphael.circle(e,t,n).attr("fill",r).attr("stroke-width",this.strokeWidthForSeries(i)).attr("stroke",this.strokeForSeries(i))},n.prototype.strokeWidthForSeries=function(e){return this.options.pointWidths[e%this.options.pointWidths.length]},n.prototype.strokeForSeries=function(e){return this.options.pointStrokeColors[e%this.options.pointStrokeColors.length]},n}(t.Grid),t.labelSeries=function(n,r,i,s,o){var u,a,f,l,c,h,p,d,v,m,g;f=200*(r-n)/i,a=new Date(n),p=t.LABEL_SPECS[s];if(p===void 0){g=t.AUTO_LABEL_ORDER;for(v=0,m=g.length;v=h.span){p=h;break}}}p===void 0&&(p=t.LABEL_SPECS.second),o&&(p=e.extend({},p,{fmt:o})),u=p.start(a),c=[];while((d=u.getTime())<=r)d>=n&&c.push([p.fmt(u),d]),p.incr(u);return c},n=function(e){return{span:e*60*1e3,start:function(e){return new Date(e.getFullYear(),e.getMonth(),e.getDate(),e.getHours())},fmt:function(e){return""+t.pad2(e.getHours())+":"+t.pad2(e.getMinutes())},incr:function(t){return t.setUTCMinutes(t.getUTCMinutes()+e)}}},r=function(e){return{span:e*1e3,start:function(e){return new Date(e.getFullYear(),e.getMonth(),e.getDate(),e.getHours(),e.getMinutes())},fmt:function(e){return""+t.pad2(e.getHours())+":"+t.pad2(e.getMinutes())+":"+t.pad2(e.getSeconds())},incr:function(t){return t.setUTCSeconds(t.getUTCSeconds()+e)}}},t.LABEL_SPECS={decade:{span:1728e8,start:function(e){return new Date(e.getFullYear()-e.getFullYear()%10,0,1)},fmt:function(e){return""+e.getFullYear()},incr:function(e){return e.setFullYear(e.getFullYear()+10)}},year:{span:1728e7,start:function(e){return new Date(e.getFullYear(),0,1)},fmt:function(e){return""+e.getFullYear()},incr:function(e){return e.setFullYear(e.getFullYear()+1)}},month:{span:24192e5,start:function(e){return new Date(e.getFullYear(),e.getMonth(),1)},fmt:function(e){return""+e.getFullYear()+"-"+t.pad2(e.getMonth()+1)},incr:function(e){return e.setMonth(e.getMonth()+1)}},day:{span:864e5,start:function(e){return new Date(e.getFullYear(),e.getMonth(),e.getDate())},fmt:function(e){return""+e.getFullYear()+"-"+t.pad2(e.getMonth()+1)+"-"+t.pad2(e.getDate())},incr:function(e){return e.setDate(e.getDate()+1)}},hour:n(60),"30min":n(30),"15min":n(15),"10min":n(10),"5min":n(5),minute:n(1),"30sec":r(30),"15sec":r(15),"10sec":r(10),"5sec":r(5),second:r(1)},t.AUTO_LABEL_ORDER=["decade","year","month","day","hour","30min","15min","10min","5min","minute","30sec","15sec","10sec","5sec","second"],t.Area=function(n){function i(n){var s;if(!(this instanceof t.Area))return new t.Area(n);s=e.extend({},r,n),this.cumulative=!s.behaveLikeLine,s.fillOpacity==="auto"&&(s.fillOpacity=s.behaveLikeLine?.8:1),i.__super__.constructor.call(this,s)}var r;return o(i,n),r={fillOpacity:"auto",behaveLikeLine:!1},i.prototype.calcPoints=function(){var e,t,n,r,i,s,o;s=this.data,o=[];for(r=0,i=s.length;r=t;0<=t?e++:e--)a.push(e);return a}.apply(this):t=function(){f=[];for(var e=u=this.options.ykeys.length-1;u<=0?e<=0:e>=0;u<=0?e++:e--)f.push(e);return f}.apply(this),l=[];for(i=0,s=t.length;ic;e=0<=c?++l:--l)u=this.data[this.data.length-1-e],t=this.drawXAxisLabel(u._x,f,u.label),a=t.getBBox(),t.transform("r"+ -this.options.xLabelAngle),n=t.getBBox(),t.transform("t0,"+n.height/2+"..."),this.options.xLabelAngle!==0&&(i=-0.5*a.width*Math.cos(this.options.xLabelAngle*Math.PI/180),t.transform("t"+i+",0...")),(o==null||o>=n.x+n.width||s!=null&&s>=n.x)&&n.x>=0&&n.x+n.width=0?this.transY(0):null,this.bars=function(){var u,d,v,m;v=this.data,m=[];for(r=u=0,d=v.length;u"+r.label+"",a=r.y;for(n=o=0,u=a.length;o\n "+this.options.labels[n]+":\n "+this.yLabelFormat(s)+"\n";return typeof this.options.hoverCallback=="function"&&(t=this.options.hoverCallback(e,this.options,t)),i=this.left+(e+.5)*this.width/this.data.length,[t,i]},r.prototype.drawXAxisLabel=function(e,t,n){var r;return r=this.raphael.text(e,t,n).attr("font-size",this.options.gridTextSize).attr("font-family",this.options.gridTextFamily).attr("font-weight",this.options.gridTextWeight).attr("fill",this.options.gridTextColor)},r.prototype.drawBar=function(e,t,n,r,i){return this.raphael.rect(e,t,n,r).attr("fill",i).attr("stroke-width",0)},r}(t.Grid),t.Donut=function(n){function r(n){this.select=u(this.select,this),this.click=u(this.click,this);var r;if(!(this instanceof t.Donut))return new t.Donut(n);typeof n.element=="string"?this.el=e(document.getElementById(n.element)):this.el=e(n.element),this.options=e.extend({},this.defaults,n);if(this.el===null||this.el.length===0)throw new Error("Graph placeholder not found.");if(n.data===void 0||n.data.length===0)return;this.data=n.data,this.values=function(){var e,t,n,i;n=this.data,i=[];for(e=0,t=n.length;eMath.PI?1:0,this.path=this.calcSegment(this.inner+3,this.inner+this.outer-5),this.selectedPath=this.calcSegment(this.inner+3,this.inner+this.outer),this.hilight=this.calcArc(this.inner)}return o(t,e),t.prototype.calcArcPoints=function(e){return[this.cx+e*this.sin_p0,this.cy+e*this.cos_p0,this.cx+e*this.sin_p1,this.cy+e*this.cos_p1]},t.prototype.calcSegment=function(e,t){var n,r,i,s,o,u,a,f,l,c;return l=this.calcArcPoints(e),n=l[0],i=l[1],r=l[2],s=l[3],c=this.calcArcPoints(t),o=c[0],a=c[1],u=c[2],f=c[3],"M"+n+","+i+("A"+e+","+e+",0,"+this.is_long+",0,"+r+","+s)+("L"+u+","+f)+("A"+t+","+t+",0,"+this.is_long+",1,"+o+","+a)+"Z"},t.prototype.calcArc=function(e){var t,n,r,i,s;return s=this.calcArcPoints(e),t=s[0],r=s[1],n=s[2],i=s[3],"M"+t+","+r+("A"+e+","+e+",0,"+this.is_long+",0,"+n+","+i)},t.prototype.render=function(){var e=this;return this.arc=this.drawDonutArc(this.hilight,this.color),this.seg=this.drawDonutSegment(this.path,this.color,this.backgroundColor,function(){return e.fire("hover",e.index)},function(){return e.fire("click",e.index)})},t.prototype.drawDonutArc=function(e,t){return this.raphael.path(e).attr({stroke:t,"stroke-width":2,opacity:0})},t.prototype.drawDonutSegment=function(e,t,n,r,i){return this.raphael.path(e).attr({fill:t,stroke:n,"stroke-width":3}).hover(r).click(i)},t.prototype.select=function(){if(!this.selected)return this.seg.animate({path:this.selectedPath},150,"<>"),this.arc.animate({opacity:1},150,"<>"),this.selected=!0},t.prototype.deselect=function(){if(this.selected)return this.seg.animate({path:this.path},150,"<>"),this.arc.animate({opacity:0},150,"<>"),this.selected=!1},t}(t.EventEmitter)}).call(this); -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | --------------------------------------------------------------------------------