app/frontend/javascript/pipeline-graph/index.js
// 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)
})
}