sabisu 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +191 -0
- data/README.md +72 -0
- data/bin/sabisu +39 -0
- data/lib/sabisu.rb +5 -0
- data/lib/sabisu/config.rb +116 -0
- data/lib/sabisu/event.rb +184 -0
- data/lib/sabisu/public/css/base.css +32 -0
- data/lib/sabisu/public/css/bootstrap.min.css +9 -0
- data/lib/sabisu/public/css/events.css +194 -0
- data/lib/sabisu/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/lib/sabisu/public/fonts/glyphicons-halflings-regular.svg +228 -0
- data/lib/sabisu/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/lib/sabisu/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/lib/sabisu/public/js/sabisu.js +893 -0
- data/lib/sabisu/public/js/typeahead.js +1718 -0
- data/lib/sabisu/routes/api.rb +38 -0
- data/lib/sabisu/routes/client.rb +13 -0
- data/lib/sabisu/routes/events.rb +9 -0
- data/lib/sabisu/routes/init.rb +4 -0
- data/lib/sabisu/routes/sensu.rb +44 -0
- data/lib/sabisu/sensu.rb +67 -0
- data/lib/sabisu/server.rb +110 -0
- data/lib/sabisu/templates/events.haml +268 -0
- data/lib/sabisu/templates/layout.haml +85 -0
- data/lib/sabisu/templates/login.haml +22 -0
- data/lib/sabisu/version.rb +4 -0
- metadata +284 -0
| @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            # api routes
         | 
| 2 | 
            +
            module Sabisu
         | 
| 3 | 
            +
              # server class
         | 
| 4 | 
            +
              class Server
         | 
| 5 | 
            +
                get '/api/events' do
         | 
| 6 | 
            +
                  params = request.env['rack.request.query_hash']
         | 
| 7 | 
            +
                  events = Event.all(params)
         | 
| 8 | 
            +
                  JSON.pretty_generate(events)
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                get '/api/events/search' do
         | 
| 12 | 
            +
                  params = request.env['rack.request.query_hash']
         | 
| 13 | 
            +
                  if params.key?('query')
         | 
| 14 | 
            +
                    query = params['query']
         | 
| 15 | 
            +
                    params.delete('query')
         | 
| 16 | 
            +
                  else
         | 
| 17 | 
            +
                    return 'Must supply \'query\' parameter'
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
                  events = Event.search(query, params)
         | 
| 20 | 
            +
                  JSON.pretty_generate(events)
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                get '/api/events/stale' do
         | 
| 24 | 
            +
                  params = request.env['rack.request.query_hash']
         | 
| 25 | 
            +
                  stale = Event.stale(params)
         | 
| 26 | 
            +
                  JSON.pretty_generate(stale: stale)
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                get '/api/events/changes' do
         | 
| 30 | 
            +
                  params = request.env['rack.request.query_hash']
         | 
| 31 | 
            +
                  JSON.pretty_generate(CURRENT_DB.changes(params))
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                get '/api/configuration/fields' do
         | 
| 35 | 
            +
                  JSON.pretty_generate(FIELDS)
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
| @@ -0,0 +1,13 @@ | |
| 1 | 
            +
            # client routes
         | 
| 2 | 
            +
            module Sabisu
         | 
| 3 | 
            +
              # server class
         | 
| 4 | 
            +
              class Server
         | 
| 5 | 
            +
                get '/clients/:client' do
         | 
| 6 | 
            +
                  haml :client, locals: { client: params[:client] }
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                get '/clients/:client/:check' do
         | 
| 10 | 
            +
                  haml :client_check, locals: { client: params[:client], check: params[:check] }
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
            end
         | 
| @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            # sensu routes
         | 
| 2 | 
            +
            module Sabisu
         | 
| 3 | 
            +
              # server class
         | 
| 4 | 
            +
              class Server
         | 
| 5 | 
            +
                def sensu(request)
         | 
| 6 | 
            +
                  sensu = Sensu.new
         | 
| 7 | 
            +
                  tmp_path = request.path_info.split('/')
         | 
| 8 | 
            +
                  tmp_path.delete_at(1)
         | 
| 9 | 
            +
                  path = tmp_path.join('/')
         | 
| 10 | 
            +
                  opts = {
         | 
| 11 | 
            +
                    path: path,
         | 
| 12 | 
            +
                    method: request.request_method,
         | 
| 13 | 
            +
                    ssl: API_SSL
         | 
| 14 | 
            +
                  }
         | 
| 15 | 
            +
                  begin
         | 
| 16 | 
            +
                    opts[:payload] = JSON.parse(request.body.read) if request.post?
         | 
| 17 | 
            +
                  rescue StandardError
         | 
| 18 | 
            +
                    puts "unable to parse: #{request.body.read}"
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
                  sensu.request(opts)
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                route :get, :post, '/sensu/stashes' do
         | 
| 24 | 
            +
                  res = sensu(request)
         | 
| 25 | 
            +
                  status res.code
         | 
| 26 | 
            +
                  headers 'content-type' => 'application/json'
         | 
| 27 | 
            +
                  body res.body
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                delete '/sensu/stashes/*' do
         | 
| 31 | 
            +
                  res = sensu(request)
         | 
| 32 | 
            +
                  status res.code
         | 
| 33 | 
            +
                  headers 'content-type' => 'application/json'
         | 
| 34 | 
            +
                  body res.body
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                post '/sensu/resolve' do
         | 
| 38 | 
            +
                  res = sensu(request)
         | 
| 39 | 
            +
                  status res.code
         | 
| 40 | 
            +
                  headers 'content-type' => 'application/json'
         | 
| 41 | 
            +
                  body res.body
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
    
        data/lib/sabisu/sensu.rb
    ADDED
    
    | @@ -0,0 +1,67 @@ | |
| 1 | 
            +
            ## Heavily borrowed from sensu-cli
         | 
| 2 | 
            +
            ## https://github.com/agent462/sensu-cli/blob/master/lib/sensu-cli/api.rb
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require 'net/https'
         | 
| 5 | 
            +
            require 'json'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            # make api requests to sensu
         | 
| 8 | 
            +
            module Sabisu
         | 
| 9 | 
            +
              class Server
         | 
| 10 | 
            +
                # sensu passthrough api
         | 
| 11 | 
            +
                class Sensu
         | 
| 12 | 
            +
                  def request(opts)
         | 
| 13 | 
            +
                    http = Net::HTTP.new(API_URL, API_PORT)
         | 
| 14 | 
            +
                    http.read_timeout = 15
         | 
| 15 | 
            +
                    http.open_timeout = 5
         | 
| 16 | 
            +
                    if API_SSL == true
         | 
| 17 | 
            +
                      http.use_ssl = true
         | 
| 18 | 
            +
                      http.verify_mode = OpenSSL::SSL::VERIFY_NONE
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
                    proxy_header = { 'api-proxy' => 'true' }
         | 
| 21 | 
            +
                    case opts[:method].upcase
         | 
| 22 | 
            +
                    when 'GET'
         | 
| 23 | 
            +
                      req =  Net::HTTP::Get.new(opts[:path], proxy_header)
         | 
| 24 | 
            +
                    when 'DELETE'
         | 
| 25 | 
            +
                      req =  Net::HTTP::Delete.new(opts[:path], proxy_header)
         | 
| 26 | 
            +
                    when 'POST'
         | 
| 27 | 
            +
                      req =  Net::HTTP::Post.new(
         | 
| 28 | 
            +
                        opts[:path],
         | 
| 29 | 
            +
                        proxy_header.merge!('Content-Type' => 'application/json')
         | 
| 30 | 
            +
                      )
         | 
| 31 | 
            +
                      req.body = opts[:payload].to_json
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                    req.basic_auth(API_USER, API_PASSWORD) unless API_USER.nil? && API_PASSWORD.nil?
         | 
| 34 | 
            +
                    http.request(req)
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  def response(code, body, command = nil)
         | 
| 38 | 
            +
                    case code
         | 
| 39 | 
            +
                    when '200'
         | 
| 40 | 
            +
                      JSON.parse(body)
         | 
| 41 | 
            +
                    when '201'
         | 
| 42 | 
            +
                      puts 'The stash has been created.' if command == 'stashes' || command == 'silence'
         | 
| 43 | 
            +
                    when '202'
         | 
| 44 | 
            +
                      puts 'The item was submitted for processing.'
         | 
| 45 | 
            +
                    when '204'
         | 
| 46 | 
            +
                      puts 'Sensu is healthy' if command == 'health'
         | 
| 47 | 
            +
                      if command == 'aggregates' || command == 'stashes'
         | 
| 48 | 
            +
                        puts 'The item was successfully deleted.'
         | 
| 49 | 
            +
                      end
         | 
| 50 | 
            +
                    when '400'
         | 
| 51 | 
            +
                      puts 'The payload is malformed.'.color(:red)
         | 
| 52 | 
            +
                    when '401'
         | 
| 53 | 
            +
                      puts 'The request requires user authentication.'.color(:red)
         | 
| 54 | 
            +
                    when '404'
         | 
| 55 | 
            +
                      puts 'The item does not exist.'.color(:cyan)
         | 
| 56 | 
            +
                    else
         | 
| 57 | 
            +
                      if command == 'health'
         | 
| 58 | 
            +
                        puts 'Sensu is not healthy.'.color(:red)
         | 
| 59 | 
            +
                      else
         | 
| 60 | 
            +
                        puts 'There was an error while trying to complete your request. ' +
         | 
| 61 | 
            +
                             "Response code: #{code}".color(:red)
         | 
| 62 | 
            +
                      end
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
              end
         | 
| 67 | 
            +
            end
         | 
| @@ -0,0 +1,110 @@ | |
| 1 | 
            +
            gem 'thin'
         | 
| 2 | 
            +
            gem 'sinatra', '1.4.4'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            # load gems
         | 
| 5 | 
            +
            require 'sinatra'
         | 
| 6 | 
            +
            require 'sinatra/base'
         | 
| 7 | 
            +
            require 'sinatra/multi_route'
         | 
| 8 | 
            +
            require 'couchrest'
         | 
| 9 | 
            +
            require 'restclient'
         | 
| 10 | 
            +
            require 'cgi'
         | 
| 11 | 
            +
            require 'json'
         | 
| 12 | 
            +
            require 'pp'
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            # load version number
         | 
| 15 | 
            +
            require_relative 'version'
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            module Sabisu
         | 
| 18 | 
            +
              # run sabisu as a server
         | 
| 19 | 
            +
              class Server < Sinatra::Base
         | 
| 20 | 
            +
                register Sinatra::MultiRoute
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                # load classes
         | 
| 23 | 
            +
                require_relative 'sensu'
         | 
| 24 | 
            +
                require_relative 'event'
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                # load configuration settings
         | 
| 27 | 
            +
                require_relative 'config'
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                # load routes
         | 
| 30 | 
            +
                require_relative 'routes/init'
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                before '/:name' do
         | 
| 33 | 
            +
                  unless params[:name] == 'login'
         | 
| 34 | 
            +
                    session[:url] = request.fullpath
         | 
| 35 | 
            +
                    force_session_auth
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                before '/api/*' do
         | 
| 40 | 
            +
                  protected!
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                get '/' do
         | 
| 44 | 
            +
                  redirect '/login'
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                get '/login' do
         | 
| 48 | 
            +
                  if logged_in?
         | 
| 49 | 
            +
                    redirect '/events'
         | 
| 50 | 
            +
                  else
         | 
| 51 | 
            +
                    haml :login, locals: { remember_me: session[:remember_me] }
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                post '/login' do
         | 
| 56 | 
            +
                  if validate(params[:username], params['password'])
         | 
| 57 | 
            +
                    session[:logged_in] = true
         | 
| 58 | 
            +
                    session[:username] = params[:username]
         | 
| 59 | 
            +
                    session[:remember_me] = params[:username] if params[:remember_me] == 'on'
         | 
| 60 | 
            +
                    session[:url] = '/events' unless session.key?(:url)
         | 
| 61 | 
            +
                    redirect session[:url]
         | 
| 62 | 
            +
                  else
         | 
| 63 | 
            +
                    haml :login, locals: { message: 'Incorrect username and/or password' }
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                get '/logout' do
         | 
| 68 | 
            +
                  clear_session
         | 
| 69 | 
            +
                  redirect '/login'
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                helpers do
         | 
| 73 | 
            +
                  def validate(username, password)
         | 
| 74 | 
            +
                    username == UI_USERNAME && password == UI_PASSWORD
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  def logged_in?
         | 
| 78 | 
            +
                    NOAUTH == true || (session[:logged_in] == true && session[:username])
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def force_session_auth
         | 
| 82 | 
            +
                    if logged_in?
         | 
| 83 | 
            +
                      return true
         | 
| 84 | 
            +
                    else
         | 
| 85 | 
            +
                      redirect '/login'
         | 
| 86 | 
            +
                      return false
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  def clear_session
         | 
| 91 | 
            +
                    session.clear
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  def authorized?
         | 
| 95 | 
            +
                    @auth ||=  Rack::Auth::Basic::Request.new(request.env)
         | 
| 96 | 
            +
                    @auth.provided? &&
         | 
| 97 | 
            +
                      @auth.basic? &&
         | 
| 98 | 
            +
                      @auth.credentials &&
         | 
| 99 | 
            +
                      @auth.credentials == [UI_USERNAME, UI_PASSWORD]
         | 
| 100 | 
            +
                  end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                  def protected!
         | 
| 103 | 
            +
                    unless authorized? || logged_in?
         | 
| 104 | 
            +
                      response['WWW-Authenticate'] = "Basic realm='Sabisu requires authentication'"
         | 
| 105 | 
            +
                      throw(:halt, [401, "Not authorized\n"])
         | 
| 106 | 
            +
                    end
         | 
| 107 | 
            +
                  end
         | 
| 108 | 
            +
                end
         | 
| 109 | 
            +
              end
         | 
| 110 | 
            +
            end
         | 
| @@ -0,0 +1,268 @@ | |
| 1 | 
            +
            %link{:rel => 'stylesheet', :href => '/css/events.css'}
         | 
| 2 | 
            +
            %script{:src => '/js/typeahead.js'}
         | 
| 3 | 
            +
            %script{:src => 'https://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/md5.js'}
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            #eventsController.row{'ng-controller' => 'eventsController'}
         | 
| 6 | 
            +
              -# last update time in corner
         | 
| 7 | 
            +
              %span#corner_status
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              -# search box (and sort dropdown)
         | 
| 10 | 
            +
              #search.col-md-12.pull-right.main-content
         | 
| 11 | 
            +
                .panel
         | 
| 12 | 
            +
                #search_field.collapse.in{:role => 'search'}
         | 
| 13 | 
            +
                  .panel-body
         | 
| 14 | 
            +
                    %form.form-inline{:role => 'search'}
         | 
| 15 | 
            +
                      #search_form_group.form-group.col-md-9
         | 
| 16 | 
            +
                        %input#search_input.form-control{:type => 'text', :placeholder => 'Search', 'ng-model' => 'search_field', 'search-typeahead' => ''}
         | 
| 17 | 
            +
                        %small.help-block
         | 
| 18 | 
            +
                          %a{'data-toggle' => 'modal', 'data-target' => '#howdoisearch'} How do I search?
         | 
| 19 | 
            +
                      .form-group.col-md-2
         | 
| 20 | 
            +
                        %select#sort.form-control{'ng-model' => 'sort_field'}
         | 
| 21 | 
            +
                          %option{disabled: true} Sort by
         | 
| 22 | 
            +
                          %option{:value => 'client'} client
         | 
| 23 | 
            +
                          %option{:value => '-client'} client (descending)
         | 
| 24 | 
            +
                          %option{:value => 'check'} check
         | 
| 25 | 
            +
                          %option{:value => '-check'} check (descending)
         | 
| 26 | 
            +
                          %option{:value => 'status'} status
         | 
| 27 | 
            +
                          %option{:value => '-status'} status (descending)
         | 
| 28 | 
            +
                          %option{:value => 'age', :selected => 'selected'} age
         | 
| 29 | 
            +
                          %option{:value => '-age'} age (descending)
         | 
| 30 | 
            +
                          %option{:value => 'issued'} issued
         | 
| 31 | 
            +
                          %option{:value => '-issued'} issued (descending)
         | 
| 32 | 
            +
                          %option{:value => 'output'} output
         | 
| 33 | 
            +
                          %option{:value => '-output'} output (descending)
         | 
| 34 | 
            +
                      %input.btn.btn-success.pull-right{:type => 'submit', :value => 'Search', 'ng-click' => 'updateParams()'}
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              -# left sidebar with stats
         | 
| 37 | 
            +
              #stats.col-md-3.pull-left.main-content
         | 
| 38 | 
            +
                .panel
         | 
| 39 | 
            +
                  #stats_field.collapse.in{:role => 'stats'}
         | 
| 40 | 
            +
                    .panel-body{'ng-hide' => 'events.length == 0 || events_spin'}
         | 
| 41 | 
            +
                      -# stats by status
         | 
| 42 | 
            +
                      #stats_status{'ng-hide' => 'events_spin'}
         | 
| 43 | 
            +
                        #totals
         | 
| 44 | 
            +
                          %span.label.label-warning.pointer{'ng-click' => 'appendQuery(1, "status", false)'}= ' - '
         | 
| 45 | 
            +
                          %span.label.label-danger.pointer{'ng-click' => 'appendQuery(2, "status", false)'}= ' - '
         | 
| 46 | 
            +
                          %span.label.label-info.pointer{'ng-click' => 'appendQuery(3, "status", false)'}= ' - '
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                      -# stats by field
         | 
| 49 | 
            +
                      #stats_by_count{'ng-repeat' => '(name, stat) in stats'}
         | 
| 50 | 
            +
                        %h5
         | 
| 51 | 
            +
                          {{name | uppercase }} stats
         | 
| 52 | 
            +
                        %table.table.table-striped{'ng-hide' => 'events_spin'}
         | 
| 53 | 
            +
                          %tr
         | 
| 54 | 
            +
                            %td {{name}} name
         | 
| 55 | 
            +
                            %td Quantity
         | 
| 56 | 
            +
                          %tr{'ng-repeat' => 's in stat | slice:0:10'}
         | 
| 57 | 
            +
                            %td.col-md-3
         | 
| 58 | 
            +
                              %span.appendQuery{'ng-click' => 'appendQuery(s[0], name)'}
         | 
| 59 | 
            +
                                {{s[0]}}
         | 
| 60 | 
            +
                            %td.col-md-1.text-center
         | 
| 61 | 
            +
                              {{s[1]}}
         | 
| 62 | 
            +
             | 
| 63 | 
            +
              -# all events
         | 
| 64 | 
            +
              #events.col-md-9.pull-right.main-content{:style => 'margin-bottom: 100px'}
         | 
| 65 | 
            +
                .panel
         | 
| 66 | 
            +
                #events_field.collapse.in{:role => 'events'}
         | 
| 67 | 
            +
                  %ul.list-group
         | 
| 68 | 
            +
                    -# events options
         | 
| 69 | 
            +
                    .progress.progress-striped.active{'ng-show' => 'events_spin', :style => 'margin-top: 50px;'}
         | 
| 70 | 
            +
                      .progress-bar.progress-bar-success{:role => 'progressbar', 'aria-valuenow' => '100', 'aria-valuemin' => '0', 'aria-valuemax' => '100', :style => 'width: 100%'}
         | 
| 71 | 
            +
                        %span.sr-only getting events...
         | 
| 72 | 
            +
                    .text-center{'ng-hide' => 'events.length || events_spin'} No Events
         | 
| 73 | 
            +
                    .pull-right{'ng-hide' => 'events.length == 0 || events_spin'}
         | 
| 74 | 
            +
                      %span.view View
         | 
| 75 | 
            +
                      %select#limit{'ng-model' => 'limit_field'}
         | 
| 76 | 
            +
                        %option{:value => '10'} 10
         | 
| 77 | 
            +
                        %option{:value => '20'} 20
         | 
| 78 | 
            +
                        %option{:value => '50', :selected => "selected"} 50
         | 
| 79 | 
            +
                        %option{:value => '100'} 100
         | 
| 80 | 
            +
                        %option{:value => '200'} 200
         | 
| 81 | 
            +
                    %a#events-accordian-toggle.text-right.pull-left{'ng-click' => 'bulkToggleDetails()', 'ng-hide' => 'events.length == 0 || events_spin'} {{bulk}} all details
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                    -# an event
         | 
| 84 | 
            +
                    %br
         | 
| 85 | 
            +
                    %li.list-group-item{'ng-repeat' => 'event in events'}
         | 
| 86 | 
            +
                      .row
         | 
| 87 | 
            +
                        .panel
         | 
| 88 | 
            +
                          -# event header
         | 
| 89 | 
            +
                          .panel-heading.flat-heading.event_line{:panel => '{{event.client.name}}/{{event.check.name}}'}
         | 
| 90 | 
            +
                            .col-md-2
         | 
| 91 | 
            +
                              %h4
         | 
| 92 | 
            +
                                %span.label.pull-right.pointer{'class' => 'label-{{event.color}}', 'ng-click' => 'appendQuery(event.check.status, "status", false)'} {{event.wstatus}}
         | 
| 93 | 
            +
                            %h5.event_title.pull-left
         | 
| 94 | 
            +
                              %span.silenceBtn.glyphicon.glyphicon-volume-off{'ng-show' => 'event.client.silenced', 'ng-click' => 'updateSilenceDetails(event.client.silence_stash)', 'data-toggle' => 'modal', 'data-target' => '#silence_window_mini'}
         | 
| 95 | 
            +
                              %span.appendQuery{'ng-click' => 'appendQuery(event.client.name, "client")'}
         | 
| 96 | 
            +
                                {{event.client.name}}
         | 
| 97 | 
            +
                              %span= ' / '
         | 
| 98 | 
            +
                              %span.silenceBtn.glyphicon.glyphicon-volume-off{'ng-show' => 'event.check.silenced', 'ng-click' => 'updateSilenceDetails(event.check.silence_stash)', 'data-toggle' => 'modal', 'data-target' => '#silence_window_mini'}
         | 
| 99 | 
            +
                              %span.appendQuery{'ng-click' => 'appendQuery(event.check.name, "check")'}
         | 
| 100 | 
            +
                                {{event.check.name}}
         | 
| 101 | 
            +
                              %small.text-muted.muted-output
         | 
| 102 | 
            +
                                \- {{event.check.output}}
         | 
| 103 | 
            +
                            %h5
         | 
| 104 | 
            +
                              %span.glyphicon.glyphicon-collapse-down.toggleBtnIcon.pull-right{'ng-click' => 'toggleDetails(event.id)'}
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                          .panel-body.panel-collapse.collapse{:class => '{{event.showdetails}}', :id => '{{event.id}}'}
         | 
| 107 | 
            +
                            -# event actions
         | 
| 108 | 
            +
                            .dropdown.actions.pull-right
         | 
| 109 | 
            +
                              %button.btn.btn-link.dropdown-toggle{'data-toggle' => 'dropdown'}
         | 
| 110 | 
            +
                                Actions
         | 
| 111 | 
            +
                                %span.caret
         | 
| 112 | 
            +
                              %ul.dropdown-menu
         | 
| 113 | 
            +
                                %li
         | 
| 114 | 
            +
                                  %a{:href => '#', 'ng-click' => 'resolveEvent(event.client.name, event.check.name)'} Resolve
         | 
| 115 | 
            +
                                %li.divider
         | 
| 116 | 
            +
                                %li.dropdown-header Silence
         | 
| 117 | 
            +
                                %li
         | 
| 118 | 
            +
                                  %a{:href => '#', 'ng-click' => 'createSilenceDetails(event.client.name)', 'data-toggle' => 'modal', 'data-target' => '#silence_window'} Client
         | 
| 119 | 
            +
                                %li
         | 
| 120 | 
            +
                                  %a{:href => '#', 'ng-click' => 'createSilenceDetails(event.client.name, event.check.name)', 'data-toggle' => 'modal', 'data-target' => '#silence_window'} Check
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                            -# event attributes
         | 
| 123 | 
            +
                            %dl.dl-horizontal.col-md-5.pull-left
         | 
| 124 | 
            +
                              %dt.attr_title{'ng-repeat-start' => 'attr in event.attributes.left'} {{attr[0]}}
         | 
| 125 | 
            +
                              %dd.attr_value{'ng-repeat-end' => '', 'ng-bind-html' => 'attr[1]'}
         | 
| 126 | 
            +
                            %dl.dl-horizontal.col-md-5.pull-left
         | 
| 127 | 
            +
                              %dt.attr_title{'ng-repeat-start' => 'attr in event.attributes.right'} {{attr[0]}}
         | 
| 128 | 
            +
                              %dd.attr_value{'ng-repeat-end' => '', 'ng-bind-html' => 'attr[1]'}
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                            -# event output
         | 
| 131 | 
            +
                            .col-md-12.pull-left.output
         | 
| 132 | 
            +
                              {{event.check.output}}
         | 
| 133 | 
            +
             | 
| 134 | 
            +
              -# mini silence dialog (for seeing existing silences)
         | 
| 135 | 
            +
              #silence_window_mini.silence_window.modal.fade
         | 
| 136 | 
            +
                .modal-dialog
         | 
| 137 | 
            +
                  .modal-content
         | 
| 138 | 
            +
                    .modal-header
         | 
| 139 | 
            +
                      Silence Details for {{silencePath}}
         | 
| 140 | 
            +
                      %button.btn.btn-link.btn-xs.pull-right.close_popover{:type => 'button', 'data-dismiss' => 'modal'}
         | 
| 141 | 
            +
                        %span.glyphicon.glyphicon-remove
         | 
| 142 | 
            +
                        close
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                    .modal-body
         | 
| 145 | 
            +
                      %dl.dl-horizontal
         | 
| 146 | 
            +
                        %dt{'ng-show' => 'silenceCreated'} Created
         | 
| 147 | 
            +
                        %dd{'ng-show' => 'silenceCreated'} {{silenceCreated}}
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                        %dt{'ng-show' => 'silenceOwner'} Owner
         | 
| 150 | 
            +
                        %dd{'ng-show' => 'silenceOwner'} {{silenceOwner}}
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                        %dt{:class => 'text-{{silenceExpirationClass}}', 'ng-show' => 'silenceExpires'} Expires
         | 
| 153 | 
            +
                        %dd{:class => 'text-{{silenceExpirationClass}}', 'ng-show' => 'silenceExpires'} {{silenceExpires}}
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                        %dt{'ng-show' => 'silenceReason'} Reason
         | 
| 156 | 
            +
                        %dd{'ng-show' => 'silenceReason'} {{silenceReason}}
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                    .modal-footer
         | 
| 159 | 
            +
                      %button.deleteSilenceBtn.btn.btn-danger.btn-sm.pull-right{:type => 'button', 'ng-click' => "deleteSilence('{{silencePath}}')"}
         | 
| 160 | 
            +
                        %span.glyphicon.glyphicon-remove
         | 
| 161 | 
            +
                        Delete
         | 
| 162 | 
            +
             | 
| 163 | 
            +
              -# regular silence dialog (for creating a new silence)
         | 
| 164 | 
            +
              #silence_window.modal.fade
         | 
| 165 | 
            +
                .modal-dialog
         | 
| 166 | 
            +
                  .modal-content
         | 
| 167 | 
            +
                    .modal-header
         | 
| 168 | 
            +
                      %button.close.pull-right{:type => 'button', 'data-dismiss' => 'modal', 'aria-hidden' => 'true'}
         | 
| 169 | 
            +
                      %h4.modal-title Silence {{silencePath}}
         | 
| 170 | 
            +
                    .modal-body
         | 
| 171 | 
            +
                      %form#silence_form.form-horizontal{:role => 'form'}
         | 
| 172 | 
            +
                        .form-group.silence_owner.has-feedback
         | 
| 173 | 
            +
                          %label.col-sm-2.control-label{:for => 'owner'} Owner*
         | 
| 174 | 
            +
                          .col-sm-10
         | 
| 175 | 
            +
                            %input#owner.form-control{:type => 'text', :placeholder => 'Joe Smith'}
         | 
| 176 | 
            +
                        .form-group.silence_reason.has-feedback
         | 
| 177 | 
            +
                          %label.col-sm-2.control-label{:for => 'reason'} Reason*
         | 
| 178 | 
            +
                          .col-sm-10
         | 
| 179 | 
            +
                            %textarea#reason.form-control{:placeholder => 'Enter reason here', :rows => '3'}
         | 
| 180 | 
            +
                        .form-group
         | 
| 181 | 
            +
                          %label.col-sm-2.control-label{:for => 'expires'} Expiration*
         | 
| 182 | 
            +
                          .col-sm-10
         | 
| 183 | 
            +
                            .radio
         | 
| 184 | 
            +
                              %label
         | 
| 185 | 
            +
                                %input#resolve{:type => 'radio', :name => 'expiration', :value => 'resolve', :checked => ''}
         | 
| 186 | 
            +
                                On resolve
         | 
| 187 | 
            +
                                %span.glyphicon.glyphicon-question-sign{'data-toggle' => 'tooltip', 'data-placement' => 'right', :title => '"On resolve" will cause this silence to be deleted once the check clears for check, clients require all checks to clear. A minimum of 1 hour is enforced.'}
         | 
| 188 | 
            +
                            .radio
         | 
| 189 | 
            +
                              %label
         | 
| 190 | 
            +
                                %input#timer{:type => 'radio', :name => 'expiration', :value => 'timer'}
         | 
| 191 | 
            +
                                %span.pull-left Timer
         | 
| 192 | 
            +
                                .col-sm-3.silence_timer_val.has-feedback
         | 
| 193 | 
            +
                                  %input#timer_val.input-sm.form-control{:type => 'text', :name => 'expirationl', :placeholder => ''}
         | 
| 194 | 
            +
                                %span.glyphicon.glyphicon-question-sign{'data-toggle' => 'tooltip', 'data-placement' => 'right', :title => '"Timer" will cause this silence to be deleted in a predetermined time span. The expires value should be inputted as a number followed by a single character. m = minutes, h = hours, d = days, w = weeks. (examples 15m, 2h, 1d, 5w)'}
         | 
| 195 | 
            +
                            .radio
         | 
| 196 | 
            +
                              %label
         | 
| 197 | 
            +
                                %input#never{:type => 'radio', :name => 'expiration', :value => 'never'}
         | 
| 198 | 
            +
                                Never
         | 
| 199 | 
            +
                                %span.glyphicon.glyphicon-question-sign{'data-toggle' => 'tooltip', 'data-placement' => 'right', :title => '"Never" will cause this silence to, surprise surprise, never expire. It will remain silence until yourself or someone else manually deletes the silence. It is highly discourage to permanently silence anything. Its extremely rare that that is ever the correct action.'}
         | 
| 200 | 
            +
                      .modal-footer
         | 
| 201 | 
            +
                        %button.btn.btn-default.pull-right{:type => 'submit', 'ng-click' => 'saveSilence()'} Create
         | 
| 202 | 
            +
             | 
| 203 | 
            +
              -# help dialog
         | 
| 204 | 
            +
              #howdoisearch.modal.fade
         | 
| 205 | 
            +
                .modal-dialog
         | 
| 206 | 
            +
                  .modal-content
         | 
| 207 | 
            +
                    .modal-header
         | 
| 208 | 
            +
                      %button.close{:type => 'button', 'data-dismiss' => 'modal', 'aria-hidden' => 'true'}
         | 
| 209 | 
            +
                      %h4.modal-title How Do I Search?
         | 
| 210 | 
            +
                    .modal-body
         | 
| 211 | 
            +
                      %h4 Available Fields
         | 
| 212 | 
            +
                      %p.well.well-sm
         | 
| 213 | 
            +
                        {{event_fields_name | joinBy : ', '}}
         | 
| 214 | 
            +
                      %h4 Query Syntax
         | 
| 215 | 
            +
                      %h5 Examples
         | 
| 216 | 
            +
                      %table.table.table-striped
         | 
| 217 | 
            +
                        %tr
         | 
| 218 | 
            +
                          %td Desired Result
         | 
| 219 | 
            +
                          %td Query
         | 
| 220 | 
            +
                        %tr
         | 
| 221 | 
            +
                          %td For clients STARTING with "web"
         | 
| 222 | 
            +
                          %td client:web*
         | 
| 223 | 
            +
                        %tr
         | 
| 224 | 
            +
                          %td For checks ENDING with "process"
         | 
| 225 | 
            +
                          %td check:process
         | 
| 226 | 
            +
                        %tr
         | 
| 227 | 
            +
                          %td For checks MATCHING with "disk_usage"
         | 
| 228 | 
            +
                          %td check:"disk_usage"
         | 
| 229 | 
            +
                        %tr
         | 
| 230 | 
            +
                          %td For statuses warning or critical
         | 
| 231 | 
            +
                          %td status:[1 TO 2]
         | 
| 232 | 
            +
                        %tr
         | 
| 233 | 
            +
                          %td For "web" clients and critical
         | 
| 234 | 
            +
                          %td client:web* AND status:2
         | 
| 235 | 
            +
                        %tr
         | 
| 236 | 
            +
                          %td Search for everything, except checks that start with "disk"
         | 
| 237 | 
            +
                          %td *:* AND NOT check:disk*
         | 
| 238 | 
            +
                        %tr
         | 
| 239 | 
            +
                          %td For "web" or "dbs" clients and status is unknown
         | 
| 240 | 
            +
                          %td client:(web* OR db*) AND status:3
         | 
| 241 | 
            +
                        %tr
         | 
| 242 | 
            +
                          %td Full text search for "failure"
         | 
| 243 | 
            +
                          %td failure
         | 
| 244 | 
            +
                        %tr
         | 
| 245 | 
            +
                          %td Events since Jan 1, 2014
         | 
| 246 | 
            +
                          %td issued:[1388534400 TO Infinity]
         | 
| 247 | 
            +
                      %p
         | 
| 248 | 
            +
                        The Cloudant search query syntax is based on the
         | 
| 249 | 
            +
                        %a{:href => 'http://lucene.apache.org/core/4_2_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description'}
         | 
| 250 | 
            +
                          Lucene
         | 
| 251 | 
            +
                        syntax. Search queries take the form of "name:value" (unless the name is omitted, in which case they hit the default field).
         | 
| 252 | 
            +
                      %p
         | 
| 253 | 
            +
                        Queries over multiple fields can be logically combined and groups and fields can be grouped. The available logical operators are: "AND", "+", "OR", "NOT" and "-", and are case sensitive. Range queries can run over strings or numbers.
         | 
| 254 | 
            +
                      %p
         | 
| 255 | 
            +
                        If you want a fuzzy search you can run a query with "~" to find terms like the search term, for instance "look~" will find terms book and took.
         | 
| 256 | 
            +
                      %p
         | 
| 257 | 
            +
                        You can also increase the importance of a search term by using the boost character "^". This makes matches containing the term more relevant, e.g. cloudant "data layer"^4 will make results containing "data layer" 4 times more relevant. The default boost value is 1. Boost values must be positive, but can be less than 1 (e.g. 0.5 to reduce importance).
         | 
| 258 | 
            +
                      %p
         | 
| 259 | 
            +
                        Wild card searches are supported, for both single ("?") and multiple ("*") character searches. "dat?" would match date and data, "dat*" would match date, data, database, dates etc. Wildcards must come after a search term, you cannot do a query like "*base".
         | 
| 260 | 
            +
             | 
| 261 | 
            +
                      %p
         | 
| 262 | 
            +
                        The following characters require escaping if you want to search on them
         | 
| 263 | 
            +
             | 
| 264 | 
            +
                      %p.well.well-sm
         | 
| 265 | 
            +
                        + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
         | 
| 266 | 
            +
             | 
| 267 | 
            +
                    .modal-footer
         | 
| 268 | 
            +
                      %button.btn.btn-default{:type => 'button', 'data-dismiss' => 'modal'} Close
         |