<commit_before><!doctype html>
<!--
  Copyright 2012 the original author or authors.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
-->
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Informant | Trace explorer</title>
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="libs/bootstrap/2.1.1/css/bootstrap.min.css" type="text/css" />
<link rel="stylesheet" href="libs/bootstrap-datepicker/0.1-nightly-20120928-1/css/datepicker.css"
    type="text/css" />
<link rel="stylesheet" href="libs/qtip/2.0.0-nightly-20121017/jquery.qtip.min.css"
    type="text/css" />
<link rel="stylesheet" href="css/informant.css" type="text/css" />
<script type="text/javascript" src="libs/jquery/1.8.2/jquery.min.js"></script>
<script type="text/javascript" src="libs/bootstrap/2.1.1/js/bootstrap.min.js"></script>
<script type="text/javascript"
    src="libs/bootstrap-datepicker/0.1-nightly-20120928-1/js/bootstrap-datepicker.js">
</script>
<script type="text/javascript" src="libs/moment/1.7.2/moment.min.js"></script>
<script type="text/javascript" src="libs/handlebars/1.0.rc.1/handlebars.js"></script>
<!--[if lt IE 9]>
<script type="text/javascript" src="libs/flashcanvas/20110201/flashcanvas.js"></script>
<![endif]-->
<script type="text/javascript" src="libs/flot/0.8-nightly-20120912-1/jquery.flot.js"></script>
<script type="text/javascript" src="libs/flot/0.8-nightly-20120912-1/jquery.flot.time.js"></script>
<script type="text/javascript" src="libs/flot/0.8-nightly-20120912-1/jquery.flot.selection.js">
</script>
<script type="text/javascript" src="libs/flot/0.8-nightly-20120912-1/jquery.flot.navigate.js">
</script>
<script type="text/javascript" src="libs/spin/1.2.7/spin.min.js"></script>
<script type="text/javascript" src="libs/qtip/2.0.0-nightly-20121017/jquery.qtip.min.js"></script>
<script type="text/javascript" src="js/trace.js"></script>
<script type="text/javascript" src="js/informant.js"></script>
<script type="text/javascript">
  $(document).ready(function() {
    configureAjaxError()
    var plot, normalPoints, errorPoints, activePoints, highlightedItem, summaryItem
    var plotSelecting
    var options = {
      legend: { show: false },
      series: {
        points: { show: true }
      },
      grid: { hoverable: true, clickable: true },
      xaxis: { mode: 'time' },
      yaxis: { ticks: 10, zoomRange : false },
      zoom: { interactive: true, amount : 1.5 },
      colors: ['#edc240', '#cb4b4b', '#afd8f8'],
      selection: { mode: 'xy' }
    }
    // need to track window dimensions to identify real window resizes in IE which sends window
    // resize event when any element on the page is resized
    var windowWidth = $(window).width()
    var windowHeight = $(window).height()
    $(window).resize(function(e){
      // check plot in case this is a resize before initial plot is rendered
      if (plot && ($(window).width() != windowWidth || $(window).height() != windowHeight)) {
        windowWidth = $(window).width()
        windowHeight = $(window).height()
        plot = $.plot($('#placeholder'), plot.getData(), plot.getOptions())
      }})
    function plotResponseData(from, to) {
      var fromAsDate = new Date(from)
      fromAsDate.setHours(0,0,0,0)
      var zoomRangeFrom = fromAsDate.getTime()
      var zoomRangeTo = zoomRangeFrom + 24 * 60 * 60 * 1000; // zoomRangeFrom + 24 hours
      from -= new Date(from).getTimezoneOffset() * 60 * 1000
      to -= new Date(to).getTimezoneOffset() * 60 * 1000
      zoomRangeFrom -= new Date(zoomRangeFrom).getTimezoneOffset() * 60 * 1000
      zoomRangeTo -= new Date(zoomRangeTo).getTimezoneOffset() * 60 * 1000
      options.xaxis.min = from
      options.xaxis.max = to
      options.xaxis.zoomRange = [ zoomRangeFrom, zoomRangeTo ]
      options.yaxis.min = 0
      // reset yaxis max so it will be auto calculated to fit data points
      options.yaxis.max = undefined
      plot = $.plot($('#placeholder'), [normalPoints, errorPoints, activePoints], options)
      updateTimeFilter()
    }
    function itemsEqual(plotItem1, plotItem2) {
      if (!plotItem1 && !plotItem2) {
        return true
      } else if (!plotItem1) {
        return false
      } else if (!plotItem2) {
        return false
      } else {
        return plotItem1.seriesIndex == plotItem2.seriesIndex && plotItem1.dataIndex
            == plotItem2.dataIndex
      }
    }
    function hideTooltip() {
      hideSpinner('#tooltipSpinner')
      $('#placeholder').qtip('hide')
    }
    $('#placeholder').bind('plotzoom', function (event, plot) {
      // cancel any refresh in action
      if (refreshQueryString) {
        refreshQueryString = undefined
        hideSpinner('#refreshButtonSpinner')
        showAndFadeErrorMessage('#refreshButtonCancelByZoom')
      }
      updateTimeFilter()
      if (highlightedItem) {
        // highlight gets out of sync on zoom
        plot.unhighlight()
        plot.highlight(highlightedItem.seriesIndex, highlightedItem.dataIndex)
        // leave the highlight, but cancel the tooltip since it is out of sync also, the user can
        // get tooltip again by hovering over the highlighted data point
        hideTooltip()
      }
    })
    $('#placeholder').mousedown(function() {
      hideTooltip()
    })
    $(document).keyup(function(e) {
      if (e.keyCode == 27) { // esc key
        if (summaryItem) {
          // the tooltips (spinny and summary) have hide events that set summaryItem = null
          // so summaryItem must be checked before calling hideTooltip()
          hideTooltip()
        } else {
          // hitting esc when no item
          highlightedItem = null
          plot.unhighlight()
        }
      }
    })
    $('#placeholder').bind('plothover', function (event, pos, item) {
      if (plotSelecting) {
        plot.unhighlight(item.series, item.datapoint)
      }
    })
    $('#placeholder').bind('plotclick', function (event, pos, item) {
      if (item) {
        highlightedItem = item
        plot.unhighlight()
        // TODO highlight with bolder or larger outline
        plot.highlight(item.series, item.datapoint)
        showTraceDetailTooltip(item)
      }
    })
    var cancelingPlotSelection
    $(document).keyup(function(e) {
      if (e.keyCode == 27 && plotSelecting) { // esc key
        plot.clearSelection()
        cancelingPlotSelection = true
      }
    })
    $(document).mousedown(function(e) {
      // need to reset this variable at some point, now seems good
      cancelingPlotSelection = false
    })
    $('#placeholder').bind('plotselecting', function (event, ranges) {
      if (ranges) {
        // plotselecting events are triggered with null ranges parameter when '!selectionIsSane()'
        // (see jquery.flot.selection.js)
        plotSelecting = true
      }
    })
    $('#placeholder').bind('plotunselected', function (event, ranges) {
      plotSelecting = false
    })
    $('#placeholder').bind('plotselected', function (event, ranges) {
      if (cancelingPlotSelection) {
        // unfortunately, plotselected is still called after plot.clearSelection() in the keyup
        // event handler for the esc key
        plot.clearSelection()
        return
      }
      // cancel any refresh in action
      if (refreshQueryString) {
        refreshQueryString = undefined
        hideSpinner('#refreshButtonSpinner')
        showAndFadeErrorMessage('#refreshButtonCancelByZoom')
      }
      options.xaxis.min = ranges.xaxis.from
      options.xaxis.max = ranges.xaxis.to
      // round min/max to the nearest millisecond in outward direction so that they encompass the
      // requested selection
      options.yaxis.min = Math.floor(ranges.yaxis.from * 1000) / 1000
      options.yaxis.max = Math.ceil(ranges.yaxis.to * 1000) / 1000
      plot = $.plot($('#placeholder'), plot.getData(), options)
      plotSelecting = false
      updateTimeFilter()
      updateDurationFilter()
      if (highlightedItem) {
        // highlight gets out of sync on zoom
        plot.unhighlight()
        plot.highlight(highlightedItem.seriesIndex, highlightedItem.dataIndex)
        // leave the highlight, but cancel the tooltip since it is out of sync also, the user can
        // get tooltip again by hovering over the highlighted data point
        hideTooltip()
      }
    })
    function updateTimeFilter() {
      var from = Math.floor(plot.getAxes().xaxis.min)
      var to = Math.ceil(plot.getAxes().xaxis.max)
      // shift timezone
      from += new Date(from).getTimezoneOffset() * 60 * 1000
      to += new Date(to).getTimezoneOffset() * 60 * 1000
      // TODO use localized time format
      // currently momentjs provides only 'LT' for localized time but this does not include seconds
      // see http://momentjs.com/docs/#/customization/long-date-formats
      $('#timeFilter').html(moment(from).format('h:mm:ss A') + ' &nbsp; to &nbsp; '
          + moment(to).format('h:mm:ss A (Z)'))
    }
    function updateDurationFilter() {
      $('#durationLow').val(plot.getAxes().yaxis.min)
      $('#durationHigh').val(plot.getAxes().yaxis.max)
      $('#durationComparator').val('between').change()
    }
    function refresh() {
      // TODO use localized date format, see more detailed comment at datepicker construction since
      // the format needs to be sync'd between the date picker and this parsing
      var date = moment($('#dateFilter').val(), 'MM/DD/YYYY')
      var from
      var to
      if (plot) {
        from = moment(Math.floor(plot.getAxes().xaxis.min))
        to = moment(Math.ceil(plot.getAxes().xaxis.max))
        // shift timezone
        from.add('minutes', from.zone())
        to.add('minutes', to.zone())
        var fromAsDate = from.clone()
        fromAsDate.hours(0)
        fromAsDate.minutes(0)
        fromAsDate.seconds(0)
        fromAsDate.milliseconds(0)
        if (date.valueOf() == fromAsDate.valueOf()) {
          // dateFilter hasn't changed
          from = from.valueOf()
          to = to.valueOf()
        } else {
          // dateFilter has changed
          from = date.valueOf()
          to = date.valueOf() + 24 * 60 * 60 * 1000
        }
      } else {
        // plot.getAxes() is not yet available because the refresh button was hit refresh during
        // (a possibly very long) initial load and
        from = date.valueOf()
        to = date.valueOf() + 24 * 60 * 60 * 1000
      }
      var limit = $('#limitFilter').val()
      var queryString = buildQueryString()
      getTracePoints(from, to, limit, queryString, false)
    }
    function buildQueryString() {
      var queryString = ''
      var durationLow = $('#durationLow').val() * 1000
      if (durationLow) {
        queryString += '&low=' + durationLow
      }
      var durationHigh = $('#durationHigh').val() * 1000
      if (durationHigh) {
        queryString += '&high=' + durationHigh
      }
      if ($('#errorOnlyFilter').is(':checked')) {
        queryString += '&errorOnly=true'
      }
      if ($('#fineOnlyFilter').is(':checked')) {
        queryString += '&fineOnly=true'
      }
      var descriptionComparator = $('#descriptionComparator').val()
      var description = $('#descriptionFilter').val()
      if (description) {
        queryString += '&descriptionComparator=' + descriptionComparator
        queryString += '&description=' + description
      }
      var userIdComparator = $('#userIdComparator').val()
      var userId = $('#userIdFilter').val()
      if (userId) {
        queryString += '&userIdComparator=' + userIdComparator
        queryString += '&userId=' + userId
      }
      var background = $('#backgroundFilter').val()
      if (background) {
        queryString += '&background=' + background
      }
      return queryString
    }
    var refreshQueryString
    var loadDetailId
    function getTracePoints(from, to, limit, queryString, initialLoad) {
      queryString = 'from=' + from + '&to=' + to + '&limit=' + limit + queryString
      // handle crazy user clicking on the button
      if (!initialLoad && queryString == refreshQueryString) return
      if (initialLoad) {
        showSpinner('#initialLoadSpinner')
      } else {
        // hide initialLoadSpinner just in case the refresh button was hit during (a very long)
        // initial load
        hideSpinner('#initialLoadSpinner')
        if (!refreshQueryString) {
          // if refreshQueryString is defined, that means spinner is already showing
          showSpinner('#refreshButtonSpinner')
        }
      }
      if (!initialLoad) {
        refreshQueryString = queryString
      }
      $.getJSON('explorer/points?' + queryString, function(response) {
        if (initialLoad && refreshQueryString) {
          // a refresh query has been posted since this initial load
          return
        } else if (!initialLoad && refreshQueryString != queryString) {
          // a different query string has been posted since this one
          // or the refresh was 'canceled' by a zoom action
          return
        }
        refreshQueryString = undefined
        if (initialLoad) {
          hideSpinner('#initialLoadSpinner')
        } else {
          hideSpinner('#refreshButtonSpinner')
          showAndFadeSuccessMessage('#refreshSuccessMessage')
        }
        normalPoints = response.normalPoints
        errorPoints = response.errorPoints
        activePoints = response.activePoints
        if (response.limitExceeded) {
          $('#limit').text(limit)
          $('#limitExceeded').removeClass('hide')
        } else {
          $('#limitExceeded').addClass('hide')
        }
        hideTooltip()
        highlightedItem = null
        $('#detailTabContent').html('')
        // shift for timezone
        for (var i = 0; i < normalPoints.length; i++) {
          normalPoints[i][0] -= new Date(normalPoints[i][0]).getTimezoneOffset() * 60 * 1000
        }
        for (var i = 0; i < errorPoints.length; i++) {
          errorPoints[i][0] -= new Date(errorPoints[i][0]).getTimezoneOffset() * 60 * 1000
        }
        for (var i = 0; i < activePoints.length; i++) {
          activePoints[i][0] -= new Date(activePoints[i][0]).getTimezoneOffset() * 60 * 1000
        }
        plotResponseData(from, to)
      })
    }
    function showTraceDetailTooltip(item) {
      var x = item.pageX
      var y = item.pageY
      var id
      if (item.seriesIndex == 0) {
        id = normalPoints[item.dataIndex][2]
      } else if (item.seriesIndex == 1) {
        id = errorPoints[item.dataIndex][2]
      } else {
        id = activePoints[item.dataIndex][2]
      }
      var spinner = new Spinner({ lines: 10, width: 3, radius: 6, top: 2, left: 2 })
      function displaySpinner() {
        if (spinner) {
          var html = '<div class="inlineblock" id="tooltipSpinner" style="width: 35px;'
            + ' height: 31px"></div>'
          $('#placeholder').qtip({
            content: {
              text: html
            },
            position: {
              my: 'left center',
              target: [ x , y + 5 ],
              viewport: $('#placeholder')
            },
            hide: {
              event: 'unfocus'
            },
            show: {
              event: false
            },
            events: {
              hide: function() {
                summaryItem = null
              }
            }
          })
          $('#placeholder').qtip('show')
          spinner.spin($('#tooltipSpinner').get(0))
        }
      }
      // small delay so that if there is an immediate response the spinner doesn't blink
      setTimeout(displaySpinner, 100)
      summaryItem = item
      var localSummaryItem = summaryItem
      $.getJSON('explorer/summary/' + id, function(response) {
        spinner.stop()
        spinner = null
        // intentionally not using itemEquals() here to avoid edge case where user clicks on item
        // then zooms and clicks on the same item. if the first request comes back after the second
        // click, then itemEquals() will be true and it will pop up the correct summary, but at the
        // incorrect location
        if (localSummaryItem != summaryItem) {
          // too slow, the user has moved on to another summary already
          return
        }
        summaryTrace = response
        var html = traceSummaryTemplate(summaryTrace)
        var showDetailHtml = '<div style="margin-top: 0.5em">'
            + '<span tabindex="0" class="lightbtn red pad1 qtip-override" id="showDetail"'
            + ' style="padding-top: 4px; padding-bottom: 4px">show detail</span></div>'
        $('#placeholder').qtip({
          content: {
            text: '<div class="indent1">' + html + '</div>' + showDetailHtml
          },
          position: {
            my: 'left center',
            target: [ x , y + 5 ],
            viewport: $(window)
          },
          style: {
            classes: 'qtip-override'
          },
          hide: {
            event: 'unfocus'
          },
          show: {
            event: false
          },
          events: {
            hide: function() {
              summaryItem = null
            }
          }
        })
        $('#placeholder').qtip('show')
        $('#showDetail').click(function() {
          // handle crazy user clicking on the 'show detail' link
          if (id == loadDetailId) return
          if (!loadDetailId) {
            // if loadDetailId is defined, that means spinner is already showing
            showSpinner('#detailTabSpinner')
          }
          loadDetailId = id
          $('#detailTabContent').html('')
          $('a[href="#detailTabDiv"]').tab('show')
          $.getJSON('explorer/detail/' + id, function(response) {
            if (loadDetailId != id) {
              // a different id has been posted since this one
              return
            }
            loadDetailId = undefined
            detailTrace = response
            detailTrace.showExport = true
            var html = traceSummaryTemplate(detailTrace)
            hideSpinner('#detailTabSpinner')
            $('#detailTabContent').html('<div class="indent1">' + html + '</div>')
            $('#detailTabContent').append('<br>')
            var detailHtml = traceDetailTemplate(detailTrace)
            hideTooltip()
            $('#detailTabContent').append(detailHtml)
          })
          return false
        })
      })
    }
    $('a[data-toggle="tab"]').on('shown', function (e) {
      $(e.target).attr('tabindex', -1)
      $(e.relatedTarget).attr('tabindex', 0)
    })
    $('#toggleExtraFiltersButton').click(function(e) {
      $('#extraFilters').toggleClass('hide')
      if ($('#extraFilters').hasClass('hide')) {
        $('#toggleExtraFiltersButton').html('more filters')
      } else {
        $('#toggleExtraFiltersButton').html('less filters')
      }
      // without preventDefault, click triggers form submission
      e.preventDefault()
    })
    var now = new Date()
    var today = new Date()
    today.setHours(0,0,0,0)
    // TODO use bootstrap-datepicker momentjs backend when it's available and then use momentjs's
    // localized format 'moment.longDateFormat.L' both here and when parsing date
    // see https://github.com/eternicode/bootstrap-datepicker/issues/24
    $('#dateFilter').val(moment(today).format('MM/DD/YYYY'))
    $('#dateFilter').datepicker({format: 'mm/dd/yyyy', autoclose: true, todayHighlight: true})
    $('#durationComparator').change(function() {
      if ($('#durationComparator').val() == 'greater') {
        $('#durationLowDiv').removeClass('hide')
        $('#durationAndDiv').addClass('hide')
        $('#durationHighDiv').addClass('hide')
        $('#durationHigh').val('')
      } else if ($('#durationComparator').val() == 'less') {
        $('#durationLowDiv').addClass('hide')
        $('#durationLow').val('')
        $('#durationAndDiv').addClass('hide')
        $('#durationHighDiv').removeClass('hide')
      } else if ($('#durationComparator').val() == 'between') {
        $('#durationLowDiv').removeClass('hide')
        $('#durationAndDiv').removeClass('hide')
        $('#durationHighDiv').removeClass('hide')
      }
    })
    $('#refreshButton').click(refresh)
    $(".refresh-data-on-enter-key").keypress(function(event) {
      if (event.which == 13) {
        refresh()
        // without preventDefault, enter triggers 'more filters' button
        event.preventDefault()
      }
    })
    $('#zoomOut').click(function() { plot.zoomOut() })
    // get the last 2 hours, but nothing prior to today (e.g. if 'now' is 1am)
    var from = Math.max(today.getTime(), now.getTime() - 2 * 60 * 60 * 1000)
    // now + 15 minutes, this is primarily to set the right-hand border for the chart
    var to = now.getTime() + 15 * 60 * 1000
    getTracePoints(from, to, 500, '', true)
  })
</script>
</head>
<body class="pin-vertical-scrollbar">
  <div class="header">
    <span class="header-logo">
        <!-- lack of spaces on this line is important -->
        Informant</span><span class="header-page-name">Explorer</span>
    <div class="header-see-also dropdown pull-right">
      <!-- role is needed for keyboard nav to work (tab to link, then arrow key down) -->
      <a href="#" class="dropdown-toggle red" role="button" data-toggle="dropdown">see also:</a>
      <ul class="dropdown-menu" role="menu" style="margin: 0">
        <li role="menuitem"><a tabindex="-1" href="config.html">configuration</a></li>
        <li role="menuitem"><a tabindex="-1" href="admin.html">administration</a></li>
        <li role="menuitem"><a tabindex="-1" href="threads.html">thread dump</a></li>
      </ul>
    </div>
  </div>
  <br>
  <div>
    <div id="placeholder" style="height: 300px">
      <div class="button-spinner hide" id="initialLoadSpinner"
          style="width: 100%; height: 100%; margin: 0px auto"></div>
    </div>
    <div style="width: 100%; text-align: center; font-style: italic; margin: 4px 0">
      use mouse scroll wheel to zoom in and out
      (or double click to zoom in and<span tabindex="0" class="lightbtn red" id="zoomOut"
          style="font-style: italic">click here to zoom out</span>)
    </div>
    <!-- min-height to take up a little space even when no error message -->
    <div style="text-align:center; min-height: 20px">
      <div class="alert alert-error inlineblock hide" id="limitExceeded"
          style="margin: 6px auto 0px auto; padding: 8px 35px">
        <strong>
          Specified limit exceeded, only displaying the slowest <span id="limit"></span> traces
        </strong>
      </div>
    </div>
  </div>
  <br>
  <ul class="nav nav-tabs" id="tab">
    <li class="active">
      <a tabindex="-1" href="#filterTabDiv" data-toggle="tab">Filter</a>
    </li>
    <li><a href="#detailTabDiv" data-toggle="tab">Detail</a></li>
  </ul>
  <!-- overflow: visible so that stack traces can extend to the right -->
  <div class="tab-content" id="tabContent" style="padding-top: 10px; overflow: visible">
    <div class="tab-pane fade in active" id="filterTabDiv">
      <form class="form-horizontal">
        <fieldset>
          <div class="control-group">
            <label class="control-label" for="dateFilter">Date</label>
            <div class="controls">
              <input type="text" class="input-small refresh-data-on-enter-key" id="dateFilter">
            </div>
          </div>
          <div class="control-group">
            <label class="control-label">Time</label>
            <div class="controls">
              <!-- maybe bootstrap issue that padding-top is required for vertical alignment? -->
              <span class="help-inline" id="timeFilter" style="padding-top: 5px">All day</span>
            </div>
          </div>
          <div class="control-group">
            <label class="control-label">Duration</label>
            <div class="controls">
              <select class="input-medium0" id="durationComparator">
                <option value="greater">Greater than</option>
                <option value="less">Less than</option>
                <option value="between">Between</option>
              </select>
              <div class="input-append" id="durationLowDiv">
                <input type="text" class="input-mini refresh-data-on-enter-key" id="durationLow">
                <span class="add-on">seconds</span>
              </div>
              <div class="input-append hide" id="durationAndDiv">and</div>
              <div class="input-append hide" id="durationHighDiv">
                <input type="text" class="input-mini refresh-data-on-enter-key" id="durationHigh">
                <span class="add-on">seconds</span>
              </div>
            </div>
          </div>
          <div class="control-group">
            <div class="controls">
              <label class="checkbox">
                <input type="checkbox" class="input-small" id="errorOnlyFilter" value="true">
                <span class="help-inline">Display only errors</span>
              </label>
            </div>
          </div>
          <div class="control-group">
            <div class="controls">
              <label class="checkbox">
                <input type="checkbox" class="input-small" id="fineOnlyFilter" value="true">
                <span class="help-inline">
                  Display only traces with fine-grained profiling data
                </span>
              </label>
            </div>
          </div>
          <div class="hide" id="extraFilters">
            <div class="control-group">
              <label class="control-label" for="descriptionFilter">Description</label>
              <div class="controls">
                <select class="input-medium0" id="descriptionComparator">
                  <option value="begins">Begins with</option>
                  <option value="equals">Equals</option>
                  <option value="ends">Ends with</option>
                  <option value="contains">Contains</option>
                </select>
                <input type="text" class="input-medium refresh-data-on-enter-key"
                    id="descriptionFilter">
              </div>
            </div>
            <div class="control-group">
              <label class="control-label" for="userIdFilter">User ID</label>
              <div class="controls">
                <select class="input-medium0" id="userIdComparator">
                  <option value="begins">Begins with</option>
                  <option value="equals">Equals</option>
                  <option value="ends">Ends with</option>
                  <option value="contains">Contains</option>
                </select>
                <input type="text" class="input-medium refresh-data-on-enter-key" id="userIdFilter">
              </div>
            </div>
            <div class="control-group">
              <label class="control-label" for="backgroundFilter">Background</label>
              <div class="controls">
                <select class="input-xlarge0" id="backgroundFilter">
                  <option value="" selected="selected"></option>
                  <option value="false">Exclude background traces</option>
                  <option value="true">Include only background traces</option>
                </select>
              </div>
            </div>
            <div class="control-group">
              <label class="control-label" for="limitFilter">Limit</label>
              <div class="controls">
                <select class="input-small" id="limitFilter">
                  <option value="100">100</option>
                  <option value="200">200</option>
                  <option value="500" selected="selected">500</option>
                  <option value="1000">1,000</option>
                  <option value="2000">2,000</option>
                  <option value="5000">5,000</option>
                </select>
              </div>
            </div>
          </div>
          <div class="control-group">
            <div class="controls">
              <span tabindex="0" class="lightbtn clickable pad1 red" id="toggleExtraFiltersButton"
                  style="margin-left: -1em">
                more filters
              </span>
            </div>
          </div>
          <div class="control-group">
            <div class="controls">
              <span tabindex="0" class="btn clickable" id="refreshButton">Refresh</span>
              <span class="button-spinner hide" id="refreshButtonSpinner"></span>
              <span class="button-error-message hide" id="refreshButtonCancelByZoom">
                Refresh canceled by zoom action
              </span>
              <span class="button-success-message hide" id="refreshSuccessMessage">Refreshed</span>
            </div>
          </div>
        </fieldset>
      </form>
    </div>
    <div class="tab-pane fade" id="detailTabDiv">
      <div class="button-spinner hide" id="detailTabSpinner" style="margin: 20px 30px"></div>
      <div id="detailTabContent"></div>
    </div>
  </div>
  <br>
</body>
</html>
<commit_msg>Reset y-axis max on zooming in<commit_after><!doctype html>
<!--
  Copyright 2012 the original author or authors.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
-->
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Informant | Trace explorer</title>
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="libs/bootstrap/2.1.1/css/bootstrap.min.css" type="text/css" />
<link rel="stylesheet" href="libs/bootstrap-datepicker/0.1-nightly-20120928-1/css/datepicker.css"
    type="text/css" />
<link rel="stylesheet" href="libs/qtip/2.0.0-nightly-20121017/jquery.qtip.min.css"
    type="text/css" />
<link rel="stylesheet" href="css/informant.css" type="text/css" />
<script type="text/javascript" src="libs/jquery/1.8.2/jquery.min.js"></script>
<script type="text/javascript" src="libs/bootstrap/2.1.1/js/bootstrap.min.js"></script>
<script type="text/javascript"
    src="libs/bootstrap-datepicker/0.1-nightly-20120928-1/js/bootstrap-datepicker.js">
</script>
<script type="text/javascript" src="libs/moment/1.7.2/moment.min.js"></script>
<script type="text/javascript" src="libs/handlebars/1.0.rc.1/handlebars.js"></script>
<!--[if lt IE 9]>
<script type="text/javascript" src="libs/flashcanvas/20110201/flashcanvas.js"></script>
<![endif]-->
<script type="text/javascript" src="libs/flot/0.8-nightly-20120912-1/jquery.flot.js"></script>
<script type="text/javascript" src="libs/flot/0.8-nightly-20120912-1/jquery.flot.time.js"></script>
<script type="text/javascript" src="libs/flot/0.8-nightly-20120912-1/jquery.flot.selection.js">
</script>
<script type="text/javascript" src="libs/flot/0.8-nightly-20120912-1/jquery.flot.navigate.js">
</script>
<script type="text/javascript" src="libs/spin/1.2.7/spin.min.js"></script>
<script type="text/javascript" src="libs/qtip/2.0.0-nightly-20121017/jquery.qtip.min.js"></script>
<script type="text/javascript" src="js/trace.js"></script>
<script type="text/javascript" src="js/informant.js"></script>
<script type="text/javascript">
  $(document).ready(function() {
    configureAjaxError()
    var plot, normalPoints, errorPoints, activePoints, highlightedItem, summaryItem, limitHit
    var plotSelecting
    var options = {
      legend: { show: false },
      series: {
        points: { show: true }
      },
      grid: { hoverable: true, clickable: true },
      xaxis: { mode: 'time' },
      yaxis: { ticks: 10, zoomRange : false },
      zoom: { interactive: true, amount : 1.5 },
      colors: ['#edc240', '#cb4b4b', '#afd8f8'],
      selection: { mode: 'xy' }
    }
    // need to track window dimensions to identify real window resizes in IE which sends window
    // resize event when any element on the page is resized
    var windowWidth = $(window).width()
    var windowHeight = $(window).height()
    $(window).resize(function(e){
      // check plot in case this is a resize before initial plot is rendered
      if (plot && ($(window).width() != windowWidth || $(window).height() != windowHeight)) {
        windowWidth = $(window).width()
        windowHeight = $(window).height()
        plot = $.plot($('#placeholder'), [normalPoints, errorPoints, activePoints], options)
      }})
    function plotResponseData(from, to) {
      var fromAsDate = new Date(from)
      fromAsDate.setHours(0,0,0,0)
      var zoomRangeFrom = fromAsDate.getTime()
      var zoomRangeTo = zoomRangeFrom + 24 * 60 * 60 * 1000; // zoomRangeFrom + 24 hours
      from -= new Date(from).getTimezoneOffset() * 60 * 1000
      to -= new Date(to).getTimezoneOffset() * 60 * 1000
      zoomRangeFrom -= new Date(zoomRangeFrom).getTimezoneOffset() * 60 * 1000
      zoomRangeTo -= new Date(zoomRangeTo).getTimezoneOffset() * 60 * 1000
      options.xaxis.min = from
      options.xaxis.max = to
      options.xaxis.zoomRange = [ zoomRangeFrom, zoomRangeTo ]
      options.yaxis.min = 0
      // reset yaxis max so it will be auto calculated to fit data points
      options.yaxis.max = undefined
      plot = $.plot($('#placeholder'), [normalPoints, errorPoints, activePoints], options)
      updateTimeFilter()
    }
    function itemsEqual(plotItem1, plotItem2) {
      if (!plotItem1 && !plotItem2) {
        return true
      } else if (!plotItem1) {
        return false
      } else if (!plotItem2) {
        return false
      } else {
        return plotItem1.seriesIndex == plotItem2.seriesIndex && plotItem1.dataIndex
            == plotItem2.dataIndex
      }
    }
    function hideTooltip() {
      hideSpinner('#tooltipSpinner')
      $('#placeholder').qtip('hide')
    }
    $('#placeholder').bind('plotzoom', function(event, plot) {
      // cancel any refresh in action
      if (refreshQueryString) {
        refreshQueryString = undefined
        hideSpinner('#refreshButtonSpinner')
        showAndFadeErrorMessage('#refreshButtonCancelByZoom')
      }
      updateTimeFilter()
      if (highlightedItem) {
        // highlight gets out of sync on zoom
        plot.unhighlight()
        plot.highlight(highlightedItem.seriesIndex, highlightedItem.dataIndex)
        // leave the highlight, but cancel the tooltip since it is out of sync also, the user can
        // get tooltip again by hovering over the highlighted data point
        hideTooltip()
      }
    })
    $('#placeholder').mousedown(function() {
      hideTooltip()
    })
    $(document).keyup(function(e) {
      if (e.keyCode == 27) { // esc key
        if (summaryItem) {
          // the tooltips (spinny and summary) have hide events that set summaryItem = null
          // so summaryItem must be checked before calling hideTooltip()
          hideTooltip()
        } else {
          // hitting esc when no item
          highlightedItem = null
          plot.unhighlight()
        }
      }
    })
    $('#placeholder').bind('plothover', function(event, pos, item) {
      if (plotSelecting && item) {
        plot.unhighlight(item.series, item.datapoint)
      }
    })
    $('#placeholder').bind('plotclick', function(event, pos, item) {
      if (item) {
        highlightedItem = item
        plot.unhighlight()
        // TODO highlight with bolder or larger outline
        plot.highlight(item.series, item.datapoint)
        showTraceDetailTooltip(item)
      }
    })
    var cancelingPlotSelection
    $(document).keyup(function(e) {
      if (e.keyCode == 27 && plotSelecting) { // esc key
        plot.clearSelection()
        cancelingPlotSelection = true
      }
    })
    $(document).mousedown(function(e) {
      // need to reset this variable at some point, now seems good
      cancelingPlotSelection = false
    })
    $('#placeholder').bind('plotselecting', function(event, ranges) {
      if (ranges) {
        // plotselecting events are triggered with null ranges parameter when '!selectionIsSane()'
        // (see jquery.flot.selection.js)
        plotSelecting = true
      }
    })
    $('#placeholder').bind('plotunselected', function(event, ranges) {
      plotSelecting = false
    })
    $('#placeholder').bind('plotselected', function(event, ranges) {
      if (cancelingPlotSelection) {
        // unfortunately, plotselected is still called after plot.clearSelection() in the keyup
        // event handler for the esc key
        plot.clearSelection()
        return
      }
      plotSelecting = false
      // cancel any refresh in action
      if (refreshQueryString) {
        refreshQueryString = undefined
        hideSpinner('#refreshButtonSpinner')
        showAndFadeErrorMessage('#refreshButtonCancelByZoom')
      }
      var from = ranges.xaxis.from
      var to = ranges.xaxis.to
      // round min/max to the nearest millisecond in outward direction so that they encompass the
      // requested selection
      var low = Math.floor(ranges.yaxis.from * 1000) / 1000
      var high = Math.ceil(ranges.yaxis.to * 1000) / 1000
      zoomIn(from, to, low, high)
    })
    function zoomIn(from, to, low, high) {
      normalPoints = filterPoints(normalPoints, from, to, low, high)
      errorPoints = filterPoints(errorPoints, from, to, low, high)
      activePoints = filterPoints(activePoints, from, to, low, high)
      options.xaxis.min = from
      options.xaxis.max = to
      options.yaxis.min = 0
      // reset yaxis max so it will be auto calculated to fit data points
      options.yaxis.max = undefined
      plot = $.plot($('#placeholder'), [normalPoints, errorPoints, activePoints], options)
      updateTimeFilter()
      updateDurationFilter()
      if (highlightedItem) {
        // highlight gets out of sync on zoom
        plot.unhighlight()
        plot.highlight(highlightedItem.seriesIndex, highlightedItem.dataIndex)
        // leave the highlight, but cancel the tooltip since it is out of sync also, the user can
        // get tooltip again by hovering over the highlighted data point
        hideTooltip()
      }
    }
    function filterPoints(points, from, to, low, high) {
      var filteredPoints = []
      for (var i = 0; i < points.length; i++) {
        var point = points[i]
        if (point[0] >= from && point[0] <= to && point[1] >= low && point[1] <= high) {
          filteredPoints.push(point)
        }
      }
      return filteredPoints
    }
    function updateTimeFilter() {
      var from = Math.floor(plot.getAxes().xaxis.min)
      var to = Math.ceil(plot.getAxes().xaxis.max)
      // shift timezone
      from += new Date(from).getTimezoneOffset() * 60 * 1000
      to += new Date(to).getTimezoneOffset() * 60 * 1000
      // TODO use localized time format
      // currently momentjs provides only 'LT' for localized time but this does not include seconds
      // see http://momentjs.com/docs/#/customization/long-date-formats
      $('#timeFilter').html(moment(from).format('h:mm:ss A') + ' &nbsp; to &nbsp; '
          + moment(to).format('h:mm:ss A (Z)'))
    }
    function updateDurationFilter() {
      $('#durationLow').val(plot.getAxes().yaxis.min)
      $('#durationHigh').val(plot.getAxes().yaxis.max)
      $('#durationComparator').val('between').change()
    }
    function refresh() {
      // TODO use localized date format, see more detailed comment at datepicker construction since
      // the format needs to be sync'd between the date picker and this parsing
      var date = moment($('#dateFilter').val(), 'MM/DD/YYYY')
      var from
      var to
      if (plot) {
        from = moment(Math.floor(plot.getAxes().xaxis.min))
        to = moment(Math.ceil(plot.getAxes().xaxis.max))
        // shift timezone
        from.add('minutes', from.zone())
        to.add('minutes', to.zone())
        var fromAsDate = from.clone()
        fromAsDate.hours(0)
        fromAsDate.minutes(0)
        fromAsDate.seconds(0)
        fromAsDate.milliseconds(0)
        if (date.valueOf() == fromAsDate.valueOf()) {
          // dateFilter hasn't changed
          from = from.valueOf()
          to = to.valueOf()
        } else {
          // dateFilter has changed
          from = date.valueOf()
          to = date.valueOf() + 24 * 60 * 60 * 1000
        }
      } else {
        // plot.getAxes() is not yet available because the refresh button was hit refresh during
        // (a possibly very long) initial load and
        from = date.valueOf()
        to = date.valueOf() + 24 * 60 * 60 * 1000
      }
      var limit = $('#limitFilter').val()
      var queryString = buildQueryString()
      getTracePoints(from, to, limit, queryString, false)
    }
    function buildQueryString() {
      var queryString = ''
      var durationLow = $('#durationLow').val() * 1000
      if (durationLow) {
        queryString += '&low=' + durationLow
      }
      var durationHigh = $('#durationHigh').val() * 1000
      if (durationHigh) {
        queryString += '&high=' + durationHigh
      }
      if ($('#errorOnlyFilter').is(':checked')) {
        queryString += '&errorOnly=true'
      }
      if ($('#fineOnlyFilter').is(':checked')) {
        queryString += '&fineOnly=true'
      }
      var descriptionComparator = $('#descriptionComparator').val()
      var description = $('#descriptionFilter').val()
      if (description) {
        queryString += '&descriptionComparator=' + descriptionComparator
        queryString += '&description=' + description
      }
      var userIdComparator = $('#userIdComparator').val()
      var userId = $('#userIdFilter').val()
      if (userId) {
        queryString += '&userIdComparator=' + userIdComparator
        queryString += '&userId=' + userId
      }
      var background = $('#backgroundFilter').val()
      if (background) {
        queryString += '&background=' + background
      }
      return queryString
    }
    var refreshQueryString
    var loadDetailId
    function getTracePoints(from, to, limit, queryString, initialLoad) {
      queryString = 'from=' + from + '&to=' + to + '&limit=' + limit + queryString
      // handle crazy user clicking on the button
      if (!initialLoad && queryString == refreshQueryString) return
      if (initialLoad) {
        showSpinner('#initialLoadSpinner')
      } else {
        // hide initialLoadSpinner just in case the refresh button was hit during (a very long)
        // initial load
        hideSpinner('#initialLoadSpinner')
        if (!refreshQueryString) {
          // if refreshQueryString is defined, that means spinner is already showing
          showSpinner('#refreshButtonSpinner')
        }
      }
      if (!initialLoad) {
        refreshQueryString = queryString
      }
      $.getJSON('explorer/points?' + queryString, function(response) {
        if (initialLoad && refreshQueryString) {
          // a refresh query has been posted since this initial load
          return
        } else if (!initialLoad && refreshQueryString != queryString) {
          // a different query string has been posted since this one
          // or the refresh was 'canceled' by a zoom action
          return
        }
        refreshQueryString = undefined
        if (initialLoad) {
          hideSpinner('#initialLoadSpinner')
        } else {
          hideSpinner('#refreshButtonSpinner')
          showAndFadeSuccessMessage('#refreshSuccessMessage')
        }
        normalPoints = response.normalPoints
        errorPoints = response.errorPoints
        activePoints = response.activePoints
        if (response.limitExceeded) {
          $('#limit').text(limit)
          $('#limitExceeded').removeClass('hide')
        } else {
          $('#limitExceeded').addClass('hide')
        }
        hideTooltip()
        highlightedItem = null
        $('#detailTabContent').html('')
        // shift for timezone
        for (var i = 0; i < normalPoints.length; i++) {
          normalPoints[i][0] -= new Date(normalPoints[i][0]).getTimezoneOffset() * 60 * 1000
        }
        for (var i = 0; i < errorPoints.length; i++) {
          errorPoints[i][0] -= new Date(errorPoints[i][0]).getTimezoneOffset() * 60 * 1000
        }
        for (var i = 0; i < activePoints.length; i++) {
          activePoints[i][0] -= new Date(activePoints[i][0]).getTimezoneOffset() * 60 * 1000
        }
        plotResponseData(from, to)
      })
    }
    function showTraceDetailTooltip(item) {
      var x = item.pageX
      var y = item.pageY
      var id
      if (item.seriesIndex == 0) {
        id = normalPoints[item.dataIndex][2]
      } else if (item.seriesIndex == 1) {
        id = errorPoints[item.dataIndex][2]
      } else {
        id = activePoints[item.dataIndex][2]
      }
      var spinner = new Spinner({ lines: 10, width: 3, radius: 6, top: 2, left: 2 })
      function displaySpinner() {
        if (spinner) {
          var html = '<div class="inlineblock" id="tooltipSpinner" style="width: 35px;'
            + ' height: 31px"></div>'
          $('#placeholder').qtip({
            content: {
              text: html
            },
            position: {
              my: 'left center',
              target: [ x , y + 5 ],
              viewport: $('#placeholder')
            },
            hide: {
              event: 'unfocus'
            },
            show: {
              event: false
            },
            events: {
              hide: function() {
                summaryItem = null
              }
            }
          })
          $('#placeholder').qtip('show')
          spinner.spin($('#tooltipSpinner').get(0))
        }
      }
      // small delay so that if there is an immediate response the spinner doesn't blink
      setTimeout(displaySpinner, 100)
      summaryItem = item
      var localSummaryItem = summaryItem
      $.getJSON('explorer/summary/' + id, function(response) {
        spinner.stop()
        spinner = null
        // intentionally not using itemEquals() here to avoid edge case where user clicks on item
        // then zooms and clicks on the same item. if the first request comes back after the second
        // click, then itemEquals() will be true and it will pop up the correct summary, but at the
        // incorrect location
        if (localSummaryItem != summaryItem) {
          // too slow, the user has moved on to another summary already
          return
        }
        summaryTrace = response
        var html = traceSummaryTemplate(summaryTrace)
        var showDetailHtml = '<div style="margin-top: 0.5em">'
            + '<span tabindex="0" class="lightbtn red pad1 qtip-override" id="showDetail"'
            + ' style="padding-top: 4px; padding-bottom: 4px">show detail</span></div>'
        $('#placeholder').qtip({
          content: {
            text: '<div class="indent1">' + html + '</div>' + showDetailHtml
          },
          position: {
            my: 'left center',
            target: [ x , y + 5 ],
            viewport: $(window)
          },
          style: {
            classes: 'qtip-override'
          },
          hide: {
            event: 'unfocus'
          },
          show: {
            event: false
          },
          events: {
            hide: function() {
              summaryItem = null
            }
          }
        })
        $('#placeholder').qtip('show')
        $('#showDetail').click(function() {
          // handle crazy user clicking on the 'show detail' link
          if (id == loadDetailId) return
          if (!loadDetailId) {
            // if loadDetailId is defined, that means spinner is already showing
            showSpinner('#detailTabSpinner')
          }
          loadDetailId = id
          $('#detailTabContent').html('')
          $('a[href="#detailTabDiv"]').tab('show')
          $.getJSON('explorer/detail/' + id, function(response) {
            if (loadDetailId != id) {
              // a different id has been posted since this one
              return
            }
            loadDetailId = undefined
            detailTrace = response
            detailTrace.showExport = true
            var html = traceSummaryTemplate(detailTrace)
            hideSpinner('#detailTabSpinner')
            $('#detailTabContent').html('<div class="indent1">' + html + '</div>')
            $('#detailTabContent').append('<br>')
            var detailHtml = traceDetailTemplate(detailTrace)
            hideTooltip()
            $('#detailTabContent').append(detailHtml)
          })
          return false
        })
      })
    }
    $('a[data-toggle="tab"]').on('shown', function(e) {
      $(e.target).attr('tabindex', -1)
      $(e.relatedTarget).attr('tabindex', 0)
    })
    $('#toggleExtraFiltersButton').click(function(e) {
      $('#extraFilters').toggleClass('hide')
      if ($('#extraFilters').hasClass('hide')) {
        $('#toggleExtraFiltersButton').html('more filters')
      } else {
        $('#toggleExtraFiltersButton').html('less filters')
      }
      // without preventDefault, click triggers form submission
      e.preventDefault()
    })
    var now = new Date()
    var today = new Date()
    today.setHours(0,0,0,0)
    // TODO use bootstrap-datepicker momentjs backend when it's available and then use momentjs's
    // localized format 'moment.longDateFormat.L' both here and when parsing date
    // see https://github.com/eternicode/bootstrap-datepicker/issues/24
    $('#dateFilter').val(moment(today).format('MM/DD/YYYY'))
    $('#dateFilter').datepicker({format: 'mm/dd/yyyy', autoclose: true, todayHighlight: true})
    $('#durationComparator').change(function() {
      if ($('#durationComparator').val() == 'greater') {
        $('#durationLowDiv').removeClass('hide')
        $('#durationAndDiv').addClass('hide')
        $('#durationHighDiv').addClass('hide')
        $('#durationHigh').val('')
      } else if ($('#durationComparator').val() == 'less') {
        $('#durationLowDiv').addClass('hide')
        $('#durationLow').val('')
        $('#durationAndDiv').addClass('hide')
        $('#durationHighDiv').removeClass('hide')
      } else if ($('#durationComparator').val() == 'between') {
        $('#durationLowDiv').removeClass('hide')
        $('#durationAndDiv').removeClass('hide')
        $('#durationHighDiv').removeClass('hide')
      }
    })
    $('#refreshButton').click(refresh)
    $(".refresh-data-on-enter-key").keypress(function(event) {
      if (event.which == 13) {
        refresh()
        // without preventDefault, enter triggers 'more filters' button
        event.preventDefault()
      }
    })
    $('#zoomOut').click(function() { plot.zoomOut() })
    // get the last 2 hours, but nothing prior to today (e.g. if 'now' is 1am)
    var from = Math.max(today.getTime(), now.getTime() - 2 * 60 * 60 * 1000)
    // now + 15 minutes, this is primarily to set the right-hand border for the chart
    var to = now.getTime() + 15 * 60 * 1000
    getTracePoints(from, to, 500, '', true)
  })
</script>
</head>
<body class="pin-vertical-scrollbar">
  <div class="header">
    <span class="header-logo">
        <!-- lack of spaces on this line is important -->
        Informant</span><span class="header-page-name">Explorer</span>
    <div class="header-see-also dropdown pull-right">
      <!-- role is needed for keyboard nav to work (tab to link, then arrow key down) -->
      <a href="#" class="dropdown-toggle red" role="button" data-toggle="dropdown">see also:</a>
      <ul class="dropdown-menu" role="menu" style="margin: 0">
        <li role="menuitem"><a tabindex="-1" href="config.html">configuration</a></li>
        <li role="menuitem"><a tabindex="-1" href="admin.html">administration</a></li>
        <li role="menuitem"><a tabindex="-1" href="threads.html">thread dump</a></li>
      </ul>
    </div>
  </div>
  <br>
  <div>
    <div id="placeholder" style="height: 300px">
      <div class="button-spinner hide" id="initialLoadSpinner"
          style="width: 100%; height: 100%; margin: 0px auto"></div>
    </div>
    <div style="width: 100%; text-align: center; font-style: italic; margin: 4px 0">
      use mouse scroll wheel to zoom in and out
      (or double click to zoom in and<span tabindex="0" class="lightbtn red" id="zoomOut"
          style="font-style: italic">click here to zoom out</span>)
    </div>
    <!-- min-height to take up a little space even when no error message -->
    <div style="text-align:center; min-height: 20px">
      <div class="alert alert-error inlineblock hide" id="limitExceeded"
          style="margin: 6px auto 0px auto; padding: 8px 35px">
        <strong>
          Specified limit exceeded, only displaying the slowest <span id="limit"></span> traces
        </strong>
      </div>
    </div>
  </div>
  <br>
  <ul class="nav nav-tabs" id="tab">
    <li class="active">
      <a tabindex="-1" href="#filterTabDiv" data-toggle="tab">Filter</a>
    </li>
    <li><a href="#detailTabDiv" data-toggle="tab">Detail</a></li>
  </ul>
  <!-- overflow: visible so that stack traces can extend to the right -->
  <div class="tab-content" id="tabContent" style="padding-top: 10px; overflow: visible">
    <div class="tab-pane fade in active" id="filterTabDiv">
      <form class="form-horizontal">
        <fieldset>
          <div class="control-group">
            <label class="control-label" for="dateFilter">Date</label>
            <div class="controls">
              <input type="text" class="input-small refresh-data-on-enter-key" id="dateFilter">
            </div>
          </div>
          <div class="control-group">
            <label class="control-label">Time</label>
            <div class="controls">
              <!-- maybe bootstrap issue that padding-top is required for vertical alignment? -->
              <span class="help-inline" id="timeFilter" style="padding-top: 5px">All day</span>
            </div>
          </div>
          <div class="control-group">
            <label class="control-label">Duration</label>
            <div class="controls">
              <select class="input-medium0" id="durationComparator">
                <option value="greater">Greater than</option>
                <option value="less">Less than</option>
                <option value="between">Between</option>
              </select>
              <div class="input-append" id="durationLowDiv">
                <input type="text" class="input-mini refresh-data-on-enter-key" id="durationLow">
                <span class="add-on">seconds</span>
              </div>
              <div class="input-append hide" id="durationAndDiv">and</div>
              <div class="input-append hide" id="durationHighDiv">
                <input type="text" class="input-mini refresh-data-on-enter-key" id="durationHigh">
                <span class="add-on">seconds</span>
              </div>
            </div>
          </div>
          <div class="control-group">
            <div class="controls">
              <label class="checkbox">
                <input type="checkbox" class="input-small" id="errorOnlyFilter" value="true">
                <span class="help-inline">Display only errors</span>
              </label>
            </div>
          </div>
          <div class="control-group">
            <div class="controls">
              <label class="checkbox">
                <input type="checkbox" class="input-small" id="fineOnlyFilter" value="true">
                <span class="help-inline">
                  Display only traces with fine-grained profiling data
                </span>
              </label>
            </div>
          </div>
          <div class="hide" id="extraFilters">
            <div class="control-group">
              <label class="control-label" for="descriptionFilter">Description</label>
              <div class="controls">
                <select class="input-medium0" id="descriptionComparator">
                  <option value="begins">Begins with</option>
                  <option value="equals">Equals</option>
                  <option value="ends">Ends with</option>
                  <option value="contains">Contains</option>
                </select>
                <input type="text" class="input-medium refresh-data-on-enter-key"
                    id="descriptionFilter">
              </div>
            </div>
            <div class="control-group">
              <label class="control-label" for="userIdFilter">User ID</label>
              <div class="controls">
                <select class="input-medium0" id="userIdComparator">
                  <option value="begins">Begins with</option>
                  <option value="equals">Equals</option>
                  <option value="ends">Ends with</option>
                  <option value="contains">Contains</option>
                </select>
                <input type="text" class="input-medium refresh-data-on-enter-key" id="userIdFilter">
              </div>
            </div>
            <div class="control-group">
              <label class="control-label" for="backgroundFilter">Background</label>
              <div class="controls">
                <select class="input-xlarge0" id="backgroundFilter">
                  <option value="" selected="selected"></option>
                  <option value="false">Exclude background traces</option>
                  <option value="true">Include only background traces</option>
                </select>
              </div>
            </div>
            <div class="control-group">
              <label class="control-label" for="limitFilter">Limit</label>
              <div class="controls">
                <select class="input-small" id="limitFilter">
                  <option value="100">100</option>
                  <option value="200">200</option>
                  <option value="500" selected="selected">500</option>
                  <option value="1000">1,000</option>
                  <option value="2000">2,000</option>
                  <option value="5000">5,000</option>
                </select>
              </div>
            </div>
          </div>
          <div class="control-group">
            <div class="controls">
              <span tabindex="0" class="lightbtn clickable pad1 red" id="toggleExtraFiltersButton"
                  style="margin-left: -1em">
                more filters
              </span>
            </div>
          </div>
          <div class="control-group">
            <div class="controls">
              <span tabindex="0" class="btn clickable" id="refreshButton">Refresh</span>
              <span class="button-spinner hide" id="refreshButtonSpinner"></span>
              <span class="button-error-message hide" id="refreshButtonCancelByZoom">
                Refresh canceled by zoom action
              </span>
              <span class="button-success-message hide" id="refreshSuccessMessage">Refreshed</span>
            </div>
          </div>
        </fieldset>
      </form>
    </div>
    <div class="tab-pane fade" id="detailTabDiv">
      <div class="button-spinner hide" id="detailTabSpinner" style="margin: 20px 30px"></div>
      <div id="detailTabContent"></div>
    </div>
  </div>
  <br>
</body>
</html>

{'fraction_non_alphanumeric': 0.14126023022157164, 'fraction_numerical': 0.014239137667176792, 'mean_word_length': 2.3202805699768034, 'pattern_counts': {'":': 0, '<': 572, '<?xml version=': 0, '>': 565, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5626738', 'n_tokens_mistral': 17257, 'n_tokens_neox': 16594, 'n_words': 4150}