dashboard.js

/**
 * main script to build the UI
 * @module dashboard
 */
/* jslint browser: true, devel: true, node: true */
/* global $, Gauge, d3 */

let gauge = {}
let graph = {}
const width = 160
const height = 140
const interval = 5000
let x
let y
let line
let alerts = []
let alertsCurrent = {}

/**
 * setup - self-invoking function
 */
$(function setup() {
  $.get({
    url: '/measurements.json',
    cache: false,
    complete: function(returndata) {
      const measurements = JSON.parse(returndata.responseText)
      if (measurements) {
        setupDash(measurements)
        requestInterval(checkMeasurements, interval, measurements)
      }
    }
  })
})

/**
 * setupDash
 *
 * @param {object} measurements - metric for adding to the dashboard
 */
const setupDash = (measurements) => {
  measurements.forEach((i, v) => {
    $('#boards').append(`
        <div class="card-panel" id="board${v}">
          board${v}
        </div>`
    )
  })
  measurements.forEach((currentServer) => {
    const { name, location, chartType, unit } = currentServer
    let { max, scale } = currentServer
    if (!max) max = 200
    if (!scale) scale = 1
    $('#' + location).html(name + `<div class="result" id="${location}Result"></div>`)
    if (chartType === 'gauge') {
      $(`#${location}Result`).addClass('gauge-container')
      gauge[location] = Gauge(
        document.getElementById(`${location}Result`), {
          max: max,
          value: 0,
          label: (value) => { return `${Math.floor(value * scale)} ${unit || 'ms'}` }
        })
    } else if (chartType === 'spark') {
      $('#' + location).append(`<div id="${location}spark" class="aGraph"></div>`)
      graph[location] = d3.select('#' + location + 'spark').append('svg:svg').attr('width', width + 'px').attr('height', height + 'px')
      let data = currentServer.data
      x = d3.scaleLinear().domain([0, width / 2 - 2]).range([-2, width]) // starting point is -5 so the first value doesn't show and slides off the edge as part of the transition
      y = d3.scaleLinear().domain([-10, max]).range([height, 0])
      line = d3.line()
        .x((d, i) => x(i))
        .y((d) => y(d))
      graph[currentServer.location].append('svg:path').attr('d', line(data))
    }
  })
}

/**
 * get the measurements for each service
 *
 * @param {object} measurements - details of the service to measure
 */
const checkMeasurements = (measurements) => {
  measurements.forEach((currentServer) => pingServer(currentServer))
}

/**
 * measure the server response
 *
 * @param {object} server - details for measurement
 */
const pingServer = (server) => {
  const { chartType, location, data, url, value, name } = server
  let { unit, threshold } = server
  if (!unit) unit = 'ms'
  if (!threshold) threshold = 100
  $.get({
    url: url,
    start_time: new Date().getTime(),
    complete: function(returnData) {
      let result
      if (url.match(/\/api\//gi)) {
        result = JSON.parse(returnData.responseText)[value]
      } else {
        result = (new Date().getTime() - this.start_time)
      }
      $('#' + location).css('background-color', shadeBackground(result / threshold))
      if (result > threshold) {
        let alert = { type: 'alert', measure: name, value: result + unit, threshold: threshold + unit }
        if (!alertsCurrent[name]) alertsCurrent[name] = { startTime: new Date() }
        alerts.unshift(alert)
        $('#alertsRecent').prepend(`<div class="threshold threshold-recent">${new Date().toLocaleString()}: ${name} - ${result + unit} (>${threshold + unit})</div>`)
      } else {
        if (alertsCurrent[name]) delete alertsCurrent[name]
      }
      let alertsCurrentHTML = ''
      for (let [i, v] of Object.entries(alertsCurrent)) {
        const duration = Math.floor((new Date() - v.startTime) / 1000)
        alertsCurrentHTML += `<div class="threshold threshold-current">${i} ${duration.toLocaleString()}s</div>`
      }
      $('#alertsCurrent').html(alertsCurrentHTML)
      if (chartType === 'gauge') {
        gauge[location].setValueAnimated(result, 1)
      } else if (chartType === 'spark') {
        data.push(result)
        displaySpark(server, x, line)
      }
    }
  })
}

/**
 * display a spark line moving through the dashboard panel
 *
 * @param {object} server - service being measured
 * @param {number} x - x-axis value of the data
 * @param {number} line - y-axis value
 */
const displaySpark = (server, x, line) => {
  graph[server.location].selectAll('path')
    .data([server.data])
    .attr('transform', 'translate(' + x(1) + ')')
    .attr('d', line)
  if (server.data.length > width / 2 - 2) {
    server.data.shift()
    graph[server.location].selectAll('path').transition()
      .duration(interval)
      .attr('transform', 'translate(' + x(0) + ')')
  }
}

/**
 * repeat calculation of values
 *
 * @param {function} fn - callback function
 * @param {integer} delay - pause duration
 * @param {object} measurements - services being measured
 * @returns {object} handle
 */
const requestInterval = (fn, delay, measurements) => {
  const requestAnimFrame = (() => {
    return window.requestAnimationFrame || function(callback, element) {
      window.setTimeout(callback, 1000 / 60)
    }
  })()
  let start = new Date()
  let handle = {}
  const loop = () => {
    handle.value = requestAnimFrame(loop)
    const delta = new Date() - start
    if (delta >= delay) {
      fn.call(measurements, measurements)
      start = new Date()
    }
  }
  handle.value = requestAnimFrame(loop)
  return handle
}

/**
 * apply a background colour (green to red) depending on results
 *
 * @param {number} percent - percent of threshold
 * @returns {string} - colour to apply to background in HSL space
 */
const shadeBackground = (percent) => {
  if (percent > 1) percent = 1
  const hue = ((1 - percent) * 120).toString(10)
  return ['hsl(', hue, ',100%,65%)'].join('')
}