sanger/limber

View on GitHub
app/frontend/javascript/pipeline-graph/index.js

Summary

Maintainability
A
2 hrs
Test Coverage
// Entrypoint for pipeline graph page, primarily handles data loading and UI components
import colourMapping from './colourMapping.js'
import filters from './filterFunctions.js'
import graph from './graphFunctions.js'

// UI components
const filterField = document.getElementById('filter')
const showPipelineGroupsButton = document.getElementById('show-pipeline-groups')
const showPipelinesButton = document.getElementById('show-pipelines')
const pipelinesBackButton = document.getElementById('pipelines-back')

const renderPipelinesKey = function (pipelineNames) {
  const key = document.getElementById('pipelines-key')
  key.innerHTML = ''
  pipelineNames.forEach((pipeline) => {
    const item = document.createElement('li')
    const pipelineColour = colourMapping.getPipelineColour(pipeline)

    item.style.borderLeft = `solid 10px ${pipelineColour}`
    item.role = 'button'
    item.textContent = pipeline

    // when each pipeline name is hovered over, the corresponding edges are highlighted
    const pipelinesAndGroups = 'edge[pipeline = "' + pipeline + '"],edge[group = "' + pipeline + '"]'
    item.addEventListener('mouseover', () => {
      graph.getElements(pipelinesAndGroups).addClass('highlight')
    })
    item.addEventListener('mouseout', () => {
      graph.getElements(pipelinesAndGroups).removeClass('highlight')
    })

    // when each pipeline key name is clicked, filter the graph to show only that pipeline
    item.addEventListener('click', () => {
      applyFilter(pipeline)
    })

    key.appendChild(item)
  })
}

const generateTooltipContent = function (ele) {
  // pipeline properties
  const pipelineName = graph.getElementPipeline(ele)

  // purpose node properties
  const purposeName = ele.data('id')
  const purposeType = ele.data('type')
  const size = ele.data('size')
  const isInput = ele.data('input')
  const isStock = ele.data('stock')
  const isCherrypickableTarget = ele.data('cherrypickable_target')

  let content = ''
  // if element is an edge
  if (ele.isEdge()) {
    content = pipelineName
  } else {
    content = `${purposeName} [${purposeType}]`

    const properties = {
      nonStandardSize: { present: size !== null && size != 96, label: `Size: ${size}` },
      input: { present: isInput, label: 'Input' },
      stock: { present: isStock, label: 'Stock' },
      cherrypickableTarget: { present: isCherrypickableTarget, label: 'Cherrypickable Target' },
    }

    // if any properties are present
    if (Object.values(properties).some((property) => property.present)) {
      content += '<ul>'
      Object.keys(properties).forEach((property) => {
        if (properties[property].present) {
          content += `<li>${properties[property].label}</li>`
        }
      })
      content += '</ul>'
    }
  }

  return content
}

// Fetch the result of pipelines.json and then render the graph.
// pipelines.json is generated by the pipelines controller
fetch('pipelines.json').then((response) => {
  response.json().then((data) => {
    // Get filter from url
    const url = new URL(window.location.href)
    const filter = url.searchParams.get('filter')
    const group = url.searchParams.get('group')
    const showGroup = group === 'true' ? true : group === 'false' ? false : undefined // convert string to boolean

    // set before rendering the graph for the users benefit
    filterField.value = filter

    // Calculate pipeline colours
    const pipelineNames = data.pipelines.map((pipeline) => pipeline.name).sort()
    colourMapping.calculatePipelineColours(pipelineNames)
    renderPipelinesKey(pipelineNames)

    // Render the graph
    const container = document.getElementById('graph')
    graph.renderPipelines(container, data)
    applyMouseEvents()

    // Apply filter if present
    if (filter) {
      applyFilter(filter)
    }

    // Apply pipeline grouping
    applyGrouping(showGroup)
  })
})

const applyFilter = function (filter) {
  // set value in filter field
  filterField.value = filter

  // set url to reflect filter
  const url = new URL(window.location.href)
  url.searchParams.set('filter', filter)
  window.history.pushState({}, '', url)

  // set page title to reflect filter
  document.title = `Limber - Pipelines - ${filter}`

  // show or hide back button
  if (filters.hasPreviousFilter()) {
    pipelinesBackButton.classList.remove('d-none')
  } else {
    pipelinesBackButton.classList.add('d-none')
  }

  // apply filter to graph
  const results = filters.findResults(graph.getCore(), { term: filter })

  const pipelineNames = graph.getPipelineNamesFromCollection(results)
  colourMapping.calculatePipelineColours(pipelineNames)
  renderPipelinesKey(pipelineNames)

  graph.updateLayout(results)
}

const applyGrouping = function (showPipelineGroups) {
  if (showPipelineGroups !== undefined) {
    // set url to reflect filter if defined
    const url = new URL(window.location.href)
    url.searchParams.set('group', showPipelineGroups)
    window.history.pushState({}, '', url)

    // show or hide pipeline-groups buttons
    if (showPipelineGroups) {
      showPipelineGroupsButton.classList.add('d-none')
      showPipelinesButton.classList.remove('d-none')
    } else {
      showPipelineGroupsButton.classList.remove('d-none')
      showPipelinesButton.classList.add('d-none')
    }
  }

  // apply grouping to graph - uses default value in filters if showPipelineGroups is undefined
  const results = filters.findResults(graph.getCore(), { showPipelineGroups: showPipelineGroups })

  const pipelineNames = graph.getPipelineNamesFromCollection(results)
  colourMapping.calculatePipelineColours(pipelineNames)
  graph.updatePipelineColours()
  renderPipelinesKey(pipelineNames)
}

filterField.addEventListener('change', (event) => {
  const query = event.target.value
  applyFilter(query)
})

showPipelineGroupsButton.addEventListener('click', () => {
  applyGrouping(true)
})

showPipelinesButton.addEventListener('click', () => {
  applyGrouping(false)
})

pipelinesBackButton.addEventListener('click', () => {
  const previousFilter = filters.getPreviousFilter()
  applyFilter(previousFilter)
})

const applyMouseEvents = function () {
  const core = graph.getCore()

  core.elements().unbind('mouseover')
  core.elements().bind('mouseover', (event) => {
    // Highlight when mouse enters element
    event.target.addClass('highlight')

    // Add popper when mouse enters element
    event.target.popperRefObj = event.target.popper({
      content: () => {
        const content = generateTooltipContent(event.target)

        document.body.insertAdjacentHTML(
          'beforeend',
          `<div class="graph-tooltip">
            <div class="graph-tooltip-inner">
              ${content}
            </div>
          </div>`,
        )
        return document.querySelector('.graph-tooltip')
      },
    })
  })

  core.elements().unbind('mouseout')
  core.elements().bind('mouseout', (event) => {
    // Remove highlight when mouse leaves element
    event.target.removeClass('highlight')

    // Remove popper when mouse leaves element
    if (event.target.popper) {
      event.target.popperRefObj.state.elements.popper.remove()
      event.target.popperRefObj.destroy()
    }
  })

  // when an edge is clicked, filter the graph to show only that pipeline
  core.on('click', 'edge', (event) => {
    const pipeline = event.target.data('pipeline')
    const group = event.target.data('group')
    const pipelineOrGroup = pipeline || group
    applyFilter(pipelineOrGroup)
  })
}