1991 lines
61 KiB
JavaScript
1991 lines
61 KiB
JavaScript
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
// boilerplate.js -- This script should be loaded for every Robomerge page to provide a consistent UI and dataset for each page,
|
|
// as well as provide common, shared functions to our client-side Javascript.
|
|
|
|
|
|
/********************
|
|
* COMMON FUNCTIONS *
|
|
********************/
|
|
|
|
// Global user information map
|
|
let robomergeUser
|
|
|
|
window.showNodeLastChanges = false
|
|
window.allowNodeReconsiders = false
|
|
|
|
// Common mutex variable around displaying error messages
|
|
let clearer = null
|
|
function clearErrorText() {
|
|
const $err = $('#error')
|
|
$err.addClass('_hidden');
|
|
if (!clearer) {
|
|
clearer = setTimeout(() => {
|
|
if (clearer) {
|
|
$err.empty();
|
|
}
|
|
}, 1000);
|
|
}
|
|
}
|
|
function setErrorText(text) {
|
|
if (clearer) {
|
|
clearTimeout(clearer);
|
|
clearer = null;
|
|
}
|
|
$('#error').text(text).removeClass('_hidden');
|
|
}
|
|
function setError(xhr, error) {
|
|
setErrorText(createErrorMessage(xhr, error))
|
|
}
|
|
function createErrorMessage(xhr, error) {
|
|
if (xhr.responseText)
|
|
return `${xhr.status ? xhr.status + ": " : ""}${xhr.responseText}`
|
|
else if (xhr.status == 0) {
|
|
document.title = "ROBOMERGE (error)";
|
|
return "Connection error";
|
|
}
|
|
else
|
|
return `HTTP ${xhr.status}: ${error}`;
|
|
}
|
|
|
|
function promptFor(map) {
|
|
var result = {};
|
|
var success = true;
|
|
$.each(Object.keys(map), function(_, key) {
|
|
if (success) {
|
|
var p = map[key];
|
|
var dflt = "";
|
|
if (typeof(p) === "object") {
|
|
dflt = p.default || "";
|
|
p = p.prompt || key;
|
|
}
|
|
|
|
var data = prompt(p, dflt);
|
|
if (data === null) {
|
|
success = false;
|
|
}
|
|
else {
|
|
result[key] = data;
|
|
}
|
|
}
|
|
});
|
|
return success ? result : null;
|
|
}
|
|
|
|
// Add way to prompt the user on leave/reload before the perform an action
|
|
function addOnBeforeUnload(message) {
|
|
$( window ).on("beforeunload", function() {
|
|
return message
|
|
})
|
|
}
|
|
function removeOnBeforeUnload() {
|
|
$( window ).off("beforeunload")
|
|
}
|
|
|
|
function makeClLink(cl, swarmURL, alias) {
|
|
if (typeof(cl) !== "number") {
|
|
return `<span style="font-weight:bolder">${cl}</span>`
|
|
}
|
|
if (swarmURL) {
|
|
return `<a href="${swarmURL}/changes/${cl}" target="_blank">${alias || cl}</a>`
|
|
}
|
|
else {
|
|
return `${alias || cl}`
|
|
}
|
|
}
|
|
|
|
function makeSwarmFileLink(depotPath, swarmURL, alias) {
|
|
if (swarmURL) {
|
|
return `<a href="${swarmURL}/files/${
|
|
encodeURI(depotPath.endsWith('/') ? depotPath.slice(2, -1) : depotPath.slice(2)) // Remove '//' (and any trailing slash) to ensure the URL is valid
|
|
}#commits" target="_blank">${alias ? alias : depotPath}</a>`
|
|
}
|
|
else {
|
|
return `${alias ? alias : depotPath}`
|
|
}
|
|
}
|
|
|
|
function debug(message) {
|
|
log('DEBUG: ' + message)
|
|
}
|
|
|
|
function error(message) {
|
|
log('ERROR!: ' + message + '\n' + (new Error()).stack)
|
|
}
|
|
|
|
function log(message) {
|
|
const now = new Date(Date.now())
|
|
|
|
console.log(
|
|
now.toLocaleString(undefined, {dateStyle: 'short', timeStyle: 'medium', hour12: false}) +
|
|
"." + String(now.getMilliseconds()).padStart(3, '0') +
|
|
": " + message
|
|
)
|
|
}
|
|
|
|
// Reload the branchlist div
|
|
let updateBranchesPending = false;
|
|
function updateBranchList(graphBotName=null) {
|
|
if (updateBranchesPending)
|
|
return;
|
|
updateBranchesPending = true;
|
|
let expandedDivIds = []
|
|
$('#branchList .elist-row>.collapse.show').each( (_index, div) => {
|
|
expandedDivIds.push(div.id)
|
|
})
|
|
|
|
// Keep the height value pre-refresh
|
|
const beginningBLHeight = $('#branchList').height()
|
|
|
|
// Capture the current tab (if any) so we can swap back to it post-refresh if it still exists
|
|
const currentBotNavLinkId = $('.nav-item.nav-item-active>a.nav-link.botlink.active.show').attr("id")
|
|
|
|
// Prevent window resizing and scrolling
|
|
$('#branchList').css('min-height', beginningBLHeight + 'px')
|
|
getBranchList(function(data) {
|
|
if (data && data.started) {
|
|
preprocessData(data)
|
|
|
|
// clear 'error' or 'stopped' from title if present
|
|
document.title = 'ROBOMERGE';
|
|
|
|
$('div#currentlyRunning').empty()
|
|
let newBranchList = $('<div id="branchList">').append(renderBranchList(data))
|
|
expandedDivIds.forEach( (divId) => {
|
|
const escapedDivId = divId.replace(/\./g, '\\.')
|
|
// Show the div again
|
|
const div = newBranchList.find(`#${escapedDivId}`)
|
|
if (div.hasClass('collapse') && !div.hasClass('show')) {
|
|
div.addClass('show')
|
|
}
|
|
|
|
// Ensure the caret icon in the link is correct
|
|
const collapseControlIcon = newBranchList.find(`.nrow .namecell a[data-target="#${escapedDivId}"]>i`)
|
|
if (collapseControlIcon.length === 1 && collapseControlIcon.hasClass("fa-caret-right")) {
|
|
collapseControlIcon.removeClass("fa-caret-right")
|
|
collapseControlIcon.addClass("fa-caret-down")
|
|
}
|
|
})
|
|
|
|
// Prevent window resizing and scrolling
|
|
newBranchList.css('min-height', beginningBLHeight + 'px')
|
|
$('#branchList').replaceWith(newBranchList)
|
|
$('body').trigger('branchListUpdated')
|
|
}
|
|
else {
|
|
const bl = $('#branchList').empty()
|
|
|
|
$('<div>')
|
|
.text('Cannot communicate with Robomerge.')
|
|
.appendTo(bl)
|
|
|
|
$('<button>')
|
|
.addClass('btn btn-xs robomerge-crash-buttons')
|
|
.text("View Logs")
|
|
.click(function() {
|
|
window.location.href = "/api/last_crash";
|
|
})
|
|
.appendTo(bl)
|
|
}
|
|
|
|
// highlight requested link
|
|
if (graphBotName) {
|
|
const botlinkhash = `#link-${graphBotName.toUpperCase()}`
|
|
//debug(`Received request to see ${botlinkhash} on refresh`)
|
|
$(botlinkhash).click();
|
|
}
|
|
// If we found an active bot prior to this refresh, click back to it now
|
|
else if (currentBotNavLinkId && $('#' + currentBotNavLinkId).length === 1) {
|
|
//debug('Returning to bot tab with ID ' + currentBotNavLinkId)
|
|
$('#' + currentBotNavLinkId).click()
|
|
}
|
|
// Fallback to window hash if it exists
|
|
else if (window.location.hash) {
|
|
const botlinkhash = `#link-${window.location.hash.substring(1).toUpperCase()}`
|
|
//debug(`Using window.location.hash to click ${botlinkhash}`)
|
|
let bot = $(botlinkhash)
|
|
if (bot.length !== 0) {
|
|
bot.click();
|
|
}
|
|
else {
|
|
//debug(`Couldn't find a graph bot tab link for ${botlinkhash}, defaulting to first bot.`)
|
|
$('.botlink').first().click();
|
|
}
|
|
}
|
|
else {
|
|
//debug(`Defaulting to first bot.`)
|
|
$('.botlink').first().click();
|
|
}
|
|
|
|
$('#branchList').css('min-height', '')
|
|
updateBranchesPending = false;
|
|
});
|
|
}
|
|
|
|
let opPending = false;
|
|
function nodeAPIOp(botname, nodeName, op, successCallback, failureCallback) {
|
|
return genericAPIOp(`/api/op/bot/${encodeURIComponent(botname)}/node/${encodeURIComponent(nodeName)}/op${op}`, successCallback, failureCallback)
|
|
}
|
|
function edgeAPIOp(botname, nodeName, edgeName, op, successCallback, failureCallback) {
|
|
return genericAPIOp(`/api/op/bot/${encodeURIComponent(botname)}/node/${encodeURIComponent(nodeName)}/edge/${encodeURIComponent(edgeName)}/op${op}`, successCallback, failureCallback)
|
|
}
|
|
|
|
function genericAPIOp(url, successCallback, failureCallback) {
|
|
if (opPending) {
|
|
alert("Operation already pending");
|
|
return;
|
|
}
|
|
opPending = true;
|
|
clearErrorText();
|
|
|
|
return $.ajax({
|
|
url,
|
|
type: 'post',
|
|
contentType: 'application/json',
|
|
success: function(data) {
|
|
opPending = false;
|
|
if (successCallback) {
|
|
successCallback(data);
|
|
}
|
|
},
|
|
error: function(xhr, error, status) {
|
|
opPending = false;
|
|
const errMsg = createErrorMessage(xhr, error)
|
|
setErrorText(errMsg);
|
|
if (failureCallback) {
|
|
failureCallback(errMsg);
|
|
} else if (successCallback) {
|
|
// Older behavior when old code doesn't specify a failureCallback
|
|
successCallback(null);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function toQuery(queryMap) {
|
|
let queryString = "";
|
|
for (let key in queryMap) {
|
|
if (queryString.length > 0) {
|
|
queryString += "&";
|
|
}
|
|
queryString += encodeURIComponent(key) + '=' + encodeURIComponent(queryMap[key]);
|
|
}
|
|
return queryString;
|
|
}
|
|
|
|
function getBranchList(callback) {
|
|
clearErrorText();
|
|
$.ajax({
|
|
url: '/api/branches',
|
|
type: 'get',
|
|
dataType: 'json',
|
|
success: [callback, function(data) {
|
|
robomergeUser = data.user
|
|
}],
|
|
error: function(xhr, error, status) {
|
|
robomergeUser = undefined;
|
|
setError(xhr, error);
|
|
callback(null);
|
|
}
|
|
});
|
|
}
|
|
|
|
function getBranch(botname, branchname, callback) {
|
|
clearErrorText();
|
|
$.ajax({
|
|
url: `/api/bot/${botname}/branch/${branchname}`,
|
|
type: 'get',
|
|
dataType: 'json',
|
|
success: callback,
|
|
error: function(xhr, error, status) {
|
|
setError(xhr, error);
|
|
callback(null);
|
|
}
|
|
});
|
|
}
|
|
|
|
function getUserWorkspaces(serverID, callback) {
|
|
clearErrorText();
|
|
$.ajax({
|
|
url: `/api/user/${serverID}/workspaces`,
|
|
type: 'get',
|
|
dataType: 'json',
|
|
success: callback,
|
|
error: function(xhr, error, status) {
|
|
setError(xhr, error);
|
|
callback(null);
|
|
}
|
|
});
|
|
}
|
|
|
|
function handleUserPermissions(user) {
|
|
if (!user) {
|
|
return;
|
|
}
|
|
|
|
let isFTE = false;
|
|
if (user.privileges && Array.isArray(user.privileges)) {
|
|
for (const tag of user.privileges) {
|
|
if (tag === 'fte') {
|
|
isFTE = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Everyone sees when logged in
|
|
$('#p4AllBotsButton').show();
|
|
$('#trackChangeButton').show();
|
|
|
|
|
|
// Fulltime employees should see a wider set of buttons.
|
|
if (isFTE) {
|
|
$('#logButton').show();
|
|
$('#lastCrashButton').show();
|
|
$('#p4TasksButton').show();
|
|
$('#branchesButton').show();
|
|
}
|
|
}
|
|
|
|
// Adds a message along with bootstrap alert styles
|
|
// https://www.w3schools.com/bootstrap/bootstrap_ref_js_alert.asp
|
|
function displayMessage(message, alertStyle, closable = true) {
|
|
// Focus user attention
|
|
window.scrollTo(0, 0);
|
|
|
|
let messageDiv = $(`<div class="alert ${alertStyle} fade in show" role="alert">`)
|
|
|
|
// Add closable button
|
|
if (closable) {
|
|
messageDiv.addClass("alert-dismissible")
|
|
|
|
// Display 'X' button
|
|
let button = $('<button type="button" class="close" data-dismiss="alert" aria-label="Close">')
|
|
button.html('×')
|
|
messageDiv.append(button)
|
|
|
|
// Fade out after 10 seconds
|
|
setTimeout(function() {
|
|
messageDiv.slideUp().alert('close')
|
|
}, 10000)
|
|
}
|
|
|
|
messageDiv.append(message)
|
|
|
|
$('#status_message').append(messageDiv)
|
|
}
|
|
// Helper functions wrapping displayMessage()
|
|
function displaySuccessfulMessage(message, closable = true) {
|
|
displayMessage(`<strong>Success!</strong> ${message}`, "alert-success", closable)
|
|
}
|
|
function displayInfoMessage(message, closable = true) {
|
|
displayMessage(`${message}`, "alert-info", closable)
|
|
}
|
|
function displayWarningMessage(message, closable = true) {
|
|
displayMessage(`<strong>Warning:</strong> ${message}`, "alert-warning", closable)
|
|
}
|
|
function displayErrorMessage(message, closable = true) {
|
|
displayMessage(`<strong>Error:</strong> ${message}`, "alert-danger", closable)
|
|
}
|
|
|
|
// This function takes string arrays of key names (requiredKeys, optionalKeys) and attempts to get them from the query parameters.
|
|
// If successful, returns an object of { key: value, ... }
|
|
// If failure, displays an error message and returns null
|
|
function processQueryParameters(requiredKeys, optionalKeys) {
|
|
let urlParams = new URLSearchParams(window.location.search);
|
|
let returnObject = {}
|
|
|
|
requiredKeys.forEach(keyName => {
|
|
// These should have been vetted by roboserver.ts prior to getting here, but shouldn't assume
|
|
if (!urlParams.has(keyName)) {
|
|
displayErrorMessage(`Query parameter '${keyName}' required for operation. Aborting...`)
|
|
return null
|
|
} else {
|
|
returnObject[keyName] = urlParams.get(keyName)
|
|
}
|
|
});
|
|
|
|
if (optionalKeys) {
|
|
optionalKeys.forEach(keyName => {
|
|
if (urlParams.has(keyName)) {
|
|
returnObject[keyName] = urlParams.get(keyName)
|
|
}
|
|
})
|
|
}
|
|
|
|
return returnObject
|
|
}
|
|
|
|
// Creates a consistent UI for our pages
|
|
function generateRobomergeHeader(createSignedInUserDiv = true) {
|
|
// Create top-right div which contains logged-in user information and Robomerge uptime/version info
|
|
let topright = $('<div id="top-right">')
|
|
if (createSignedInUserDiv) {
|
|
let loggedInUser = $('<div id="signed-in-user" class="initiallyHidden">')
|
|
loggedInUser.append('<span><i class="fas fa-lg fa-user"></i></span>')
|
|
loggedInUser.append('<span class="user-name"></span><span class="tags"></span>')
|
|
|
|
let logOutButton = $('<button id="log-out" class="btn btn-xs btn-warning">Sign out</button>')
|
|
logOutButton.click(function() {
|
|
document.cookie = 'auth=; path=; redirect_to=;';
|
|
document.cookie = 'signedOut=true;';
|
|
window.location.href = '/login';
|
|
})
|
|
loggedInUser.append(logOutButton)
|
|
|
|
topright.append(loggedInUser)
|
|
topright.append('<div id="uptime"></div>')
|
|
topright.append('<div id="version"></div>')
|
|
}
|
|
|
|
const bigHelpButton = $('<button>')
|
|
.addClass('btn btn-outline-dark big-help')
|
|
.click(function() { window.location.href='/help' })
|
|
.html('<strong>Help Page</strong>')
|
|
topright.append(bigHelpButton)
|
|
|
|
// Show Robomerge logo
|
|
let logo = $('<img id="logoimg" src="/img/logo.png">')
|
|
.click(function() {
|
|
// Provide some secret functionality to the homepage
|
|
if (window.location.pathname === "/") {
|
|
if (event.ctrlKey) {
|
|
stopBot();
|
|
} else {
|
|
updateBranchList();
|
|
}
|
|
|
|
return false;
|
|
} else {
|
|
window.location.href = '/';
|
|
}
|
|
})
|
|
|
|
const logoDiv = $('<div id="logodiv">')
|
|
|
|
// Provide some secret functionality to the homepage
|
|
if (window.location.pathname === "/") {
|
|
logo.click(function() {
|
|
if (event.ctrlKey) {
|
|
stopBot();
|
|
} else {
|
|
updateBranchList();
|
|
}
|
|
|
|
return false;
|
|
}).appendTo(logoDiv)
|
|
}
|
|
// If we're not on the homepage, make the robomerge icon a link to the homepage
|
|
else {
|
|
$('<a href="/">').append(logo).appendTo(logoDiv)
|
|
}
|
|
|
|
// Empty div for displaying messages to the user
|
|
let statusMessageDiv = $('<div id="status_message">')
|
|
|
|
// Create header if one does not exist
|
|
let header
|
|
if ($("header").length == 0) {
|
|
$('head').after($('<header>'))
|
|
}
|
|
|
|
$('header').replaceWith($('<header>').append(topright, [logoDiv, $('<hr>'), statusMessageDiv]))
|
|
}
|
|
function generateRobomergeFooter() {
|
|
let fixedFooterContents = $('<div class="button-bar">')
|
|
|
|
let buttonDiv = $('<div id="buttons">')
|
|
fixedFooterContents.append(buttonDiv)
|
|
|
|
let logButton = $('<button id="logButton">')
|
|
logButton.addClass("btn btn-sm btn-outline-dark")
|
|
logButton.click(function() { window.open('/api/logs', '_blank') })
|
|
logButton.text("Logs")
|
|
logButton.hide()
|
|
buttonDiv.append(logButton)
|
|
|
|
let lastCrashButton = $('<button id="lastCrashButton">')
|
|
lastCrashButton.addClass("btn btn-sm btn-outline-dark")
|
|
lastCrashButton.click(function() { window.open('/api/last_crash', '_blank') })
|
|
lastCrashButton.text("Last Crash")
|
|
lastCrashButton.hide()
|
|
buttonDiv.append(lastCrashButton)
|
|
|
|
let p4TasksButton = $('<button id="p4TasksButton">')
|
|
p4TasksButton.addClass("btn btn-sm btn-outline-dark")
|
|
p4TasksButton.click(function() { window.open('/api/p4tasks', '_blank') })
|
|
p4TasksButton.text("P4 Tasks")
|
|
p4TasksButton.hide()
|
|
buttonDiv.append(p4TasksButton)
|
|
|
|
let branchesButton = $('<button id="branchesButton">')
|
|
branchesButton.addClass("btn btn-sm btn-outline-dark")
|
|
branchesButton.click(function() { window.open('/api/branches', '_blank') })
|
|
branchesButton.text("Branch Data")
|
|
branchesButton.hide()
|
|
buttonDiv.append(branchesButton)
|
|
|
|
let p4AllBotsButton = $('<button id="p4AllBotsButton">')
|
|
p4AllBotsButton.addClass("btn btn-sm btn-outline-dark")
|
|
p4AllBotsButton.click(function() { window.open('/allbots', '_blank') })
|
|
p4AllBotsButton.text("All bots graph")
|
|
p4AllBotsButton.hide()
|
|
buttonDiv.append(p4AllBotsButton)
|
|
|
|
let trackChangeButton = $('<button id="trackChangeButton">')
|
|
trackChangeButton.addClass("btn btn-sm btn-outline-dark")
|
|
trackChangeButton.click(function() {
|
|
let data = promptFor({
|
|
cl: `Enter the CL to track:`
|
|
})
|
|
if (data) {
|
|
// Ensure they entered a CL
|
|
if (!isNaN(parseInt(data.cl))) {
|
|
window.open(`/trackchange/${data.cl}`, '_blank')
|
|
}
|
|
else {
|
|
const splitCL = data.cl.split(" ")
|
|
if (splitCL.length != 2 || isNaN(splitCL[1])) {
|
|
displayErrorMessage("Please provide a valid changelist number to track.")
|
|
return
|
|
}
|
|
window.open(`/trackchange/${splitCL[0]}/${splitCL[1]}`)
|
|
}
|
|
}
|
|
})
|
|
trackChangeButton.text("Track Change")
|
|
trackChangeButton.hide()
|
|
buttonDiv.append(trackChangeButton)
|
|
|
|
let currentlyRunningDiv = $('<div id="currentlyRunning">')
|
|
fixedFooterContents.append(currentlyRunningDiv)
|
|
|
|
let rightButtonDiv = $('<div class="button-bar-right">')
|
|
fixedFooterContents.append(rightButtonDiv)
|
|
|
|
let helpButton = $('<button id="helpButton">')
|
|
helpButton.addClass("btn btn-sm btn-outline-dark")
|
|
helpButton.click(function() { window.location.href='/help' })
|
|
helpButton.html('<strong>Help Page</strong>')
|
|
rightButtonDiv.append(helpButton)
|
|
|
|
let contactButton = $('<button id="contactButton">')
|
|
contactButton.addClass("btn btn-sm btn-outline-dark")
|
|
contactButton.click(function() { window.location.href='/contact' })
|
|
contactButton.html('Contact Us')
|
|
rightButtonDiv.append(contactButton)
|
|
|
|
if ($('footer#fixed-footer').length == 0) {
|
|
$('html').append($('<footer id="fixed-footer">'))
|
|
}
|
|
|
|
$('footer#fixed-footer').replaceWith($('<footer id="fixed-footer">').append(fixedFooterContents))
|
|
}
|
|
|
|
function checkDate(beginning, end, check) {
|
|
return beginning <= check && check <= end
|
|
}
|
|
|
|
function processHolidays() {
|
|
const now = new Date()
|
|
const currentYear = now.getFullYear()
|
|
|
|
// Valentine's Day
|
|
if (checkDate(
|
|
new Date(currentYear, 1, 8, 0, 0, 0),
|
|
new Date(currentYear, 1, 15, 0, 0, 0),
|
|
now)) {
|
|
$('img#logoimg').first().attr('src', '/img/logo-valentines.png').attr('title', "Happy Valentine's Day!")
|
|
$('body').first().addClass('valentines')
|
|
}
|
|
// St. Patrick's Day
|
|
else if (checkDate(
|
|
new Date(currentYear, 2, 11, 0, 0, 0),
|
|
new Date(currentYear, 2, 18, 0, 0, 0),
|
|
now)) {
|
|
$('img#logoimg').first().attr('src', '/img/logo-stpatricks.png').attr('title', "Happy St. Paddy's Day!")
|
|
$('body').first().addClass('stpatricks')
|
|
}
|
|
// Hallowe'en
|
|
else if (checkDate(
|
|
new Date(currentYear, 9, 29, 0, 0, 0),
|
|
new Date(currentYear, 10, 2, 0, 0, 0),
|
|
now)) {
|
|
$('img#logoimg').first().attr('src', '/img/logo-halloween.png').attr('title', "Happy Halloween!")
|
|
$('body').first().addClass('halloween')
|
|
}
|
|
// Christmas
|
|
else if (checkDate(
|
|
new Date(currentYear, 11, 23, 0, 0, 0),
|
|
new Date(currentYear, 11, 28, 0, 0, 0),
|
|
now)) {
|
|
const $logo = $('img#logoimg')
|
|
$logo.first().attr('src', '/img/logo-holiday.png').attr('title', "Happy Holidays!")
|
|
$('body').first().add($logo).addClass('holiday')
|
|
}
|
|
// Star Wars Day
|
|
else if (checkDate(
|
|
new Date(currentYear, 4, 3, 0, 0, 0),
|
|
new Date(currentYear, 4, 5, 0, 0, 0),
|
|
now)) {
|
|
const $logo = $('img#logoimg')
|
|
$logo.first().attr('src', '/img/logo-starwars.png').attr('title', "May the 4th Be With You!")
|
|
$('body').first().add($logo).addClass('starwars')
|
|
}
|
|
}
|
|
|
|
/*********************************
|
|
* BRANCH TABLE HELPER FUNCTIONS *
|
|
*********************************/
|
|
|
|
// Helper functions for consistency's sake
|
|
function getGraphTableDivID(graphName) {
|
|
return `${graphName}-Table`
|
|
}
|
|
function getNodeRowId(botName, nodeName) {
|
|
return `${botName}-${nodeName}`
|
|
}
|
|
function getNodeEdgesRowId(botName, nodeName) {
|
|
return `${getNodeRowId(botName, nodeName)}-Edges`
|
|
}
|
|
function getEdgeRowId(botName, nodeName, targetBranch) {
|
|
return `${getNodeRowId(botName, nodeName)}-${targetBranch}`
|
|
}
|
|
function getAPIName(data) {
|
|
const name = data.def ? data.def.name :
|
|
data.target ? data.target :
|
|
data.name
|
|
|
|
return name.toUpperCase()
|
|
}
|
|
// If we have an explicitly set display name, use it
|
|
function getDisplayName(data) {
|
|
return data.display_name ? data.display_name :
|
|
data.def ?
|
|
(data.def.display_name ? data.def.display_name : data.def.name) :
|
|
data.name
|
|
}
|
|
|
|
function getConflict(nodeConflictArray, targetBranch=null, cl=null) {
|
|
if (!nodeConflictArray || !Array.isArray(nodeConflictArray)) {
|
|
return null
|
|
}
|
|
|
|
return nodeConflictArray.find(function (conflict) {
|
|
const ctarget = conflict.target ? conflict.target.toUpperCase() : null
|
|
return (!targetBranch || ctarget === targetBranch.toUpperCase()) &&
|
|
(!cl || conflict.cl === cl)
|
|
})
|
|
}
|
|
|
|
// Helper function to help use format duration status text
|
|
function printDurationInfo(stopString, millis) {
|
|
let time = millis / 1000; // starting off in seconds
|
|
if (time < 60) {
|
|
return [`${stopString} less than a minute ago`, 'gray'];
|
|
}
|
|
time = Math.round(time / 60);
|
|
if (time < 120) {
|
|
return [`${stopString} ${time} minute${time < 2 ? '' : 's'} ago`, time < 10 ? 'gray' : time < 20 ? 'orange' : 'red'];
|
|
}
|
|
|
|
time = Math.round(time / 60);
|
|
if (time < 36) {
|
|
return [`${stopString} ${time} hours ago`, 'red'];
|
|
}
|
|
|
|
return [`${stopString} for more than a day`, 'red'];
|
|
}
|
|
|
|
// Helper function to create action buttons
|
|
function createActionButton(buttonText, onClick, title) {
|
|
let button = $('<button class="btn btn-primary-alt">') // 'btn-primary-alt' - This is our own take on the primary button
|
|
.append(buttonText)
|
|
|
|
if (onClick) {
|
|
button.click(onClick)
|
|
}
|
|
|
|
if (title) {
|
|
button.attr('title', title)
|
|
button.attr('data-title', title)
|
|
button.attr('data-toggle', 'tooltip')
|
|
button.attr('data-placement', 'left')
|
|
button.tooltip({
|
|
trigger: 'hover',
|
|
delay: {show: 300, hide: 100}
|
|
})
|
|
button.click(function () {
|
|
$(this).tooltip('hide')
|
|
})
|
|
}
|
|
return button
|
|
|
|
}
|
|
// Helper function to create action dropdown menu items
|
|
function createActionOption(optionText, onClick, title) {
|
|
let optionLink = $('<a class="dropdown-item" href="#">').append(optionText)
|
|
|
|
if (onClick) {
|
|
optionLink.click(onClick)
|
|
}
|
|
|
|
if (title) {
|
|
optionLink.attr('title', title)
|
|
optionLink.attr('data-toggle', 'tooltip')
|
|
optionLink.attr('data-placement', 'left')
|
|
// Show slower, Hide faster -- so users can select other options without waiting for the tooltip to disappear
|
|
optionLink.tooltip({
|
|
trigger: 'hover',
|
|
delay: {show: 500, hide: 0}
|
|
})
|
|
optionLink.click(function () {
|
|
$(this).tooltip('hide')
|
|
})
|
|
}
|
|
|
|
return optionLink
|
|
}
|
|
|
|
/**************************
|
|
* BRANCH TABLE FUNCTIONS *
|
|
**************************/
|
|
|
|
let createdListeners = false
|
|
function createGraphTableDiv(graphName=null) {
|
|
let graphTableDiv = $(`<div class="gtable">`)
|
|
|
|
if (graphName) {
|
|
graphTableDiv.attr('id', getGraphTableDivID(graphName))
|
|
}
|
|
|
|
// Event listeners to show the expand and collapsed caret icon
|
|
graphTableDiv.on("show.bs.collapse hide.bs.collapse", ".collapse", function() {
|
|
const divId = $(this).attr("id")
|
|
if (!divId) {
|
|
log("ERROR: Div has no id?")
|
|
return;
|
|
}
|
|
const divLinkIconArray = $(`a[data-target="#${divId}"] i`)
|
|
if (divLinkIconArray.length === 0) {
|
|
log("ERROR: Div has no toggle link?")
|
|
return
|
|
}
|
|
|
|
const divLink = divLinkIconArray[0]
|
|
|
|
// When these events are triggered, the classes haven't been updated yet. So we check the inverse
|
|
if ($(this).hasClass("show")) {
|
|
$(divLink).removeClass("fa-caret-down")
|
|
$(divLink).addClass("fa-caret-right")
|
|
}
|
|
else {
|
|
$(divLink).removeClass("fa-caret-right")
|
|
$(divLink).addClass("fa-caret-down")
|
|
}
|
|
})
|
|
|
|
graphTableDiv.find('.nrow:nth-child(even)').addClass("nrow-zebra")
|
|
graphTableDiv.find('.erow').find(':nth-child(even)').addClass("erow-zebra")
|
|
|
|
if (!createdListeners) {
|
|
$(onDisplayTable)
|
|
window.addEventListener("resize", drawAllCanvases);
|
|
createdListeners = true
|
|
}
|
|
|
|
return graphTableDiv
|
|
}
|
|
|
|
function onDisplayTable() {
|
|
|
|
/*
|
|
* NOTE:
|
|
* Technically we don't need to stripe everytime we display a table --
|
|
* but this saves us an additional function call when finishing creating
|
|
* table rows.
|
|
*/
|
|
|
|
// CSS3 can't do nth-of-type with a class selector
|
|
$('.gtable').each(function (_index, table) {
|
|
$(table).find('.nrow:even').addClass("nrow-zebra")
|
|
})
|
|
// We want zebra striping to match the nrow it's attached to
|
|
$(".gtable .etable").each(function(index, element) {
|
|
$(element)
|
|
.find(index % 2 === 0 ? ".erow:even" : ".erow:odd")
|
|
.addClass("erow-zebra");
|
|
})
|
|
|
|
|
|
// Always redraw the canvases on display because drawing them while they are hidden
|
|
// by bootstrap nav produces buggy results
|
|
drawAllCanvases()
|
|
}
|
|
|
|
|
|
// Create a header scheme for graph table data
|
|
function createGraphTableHeaders(includeActions) {
|
|
let headerRow = $('<div class="gthead-row">')
|
|
|
|
headerRow.append($('<div class="namecell">').text('Branch Name'))
|
|
headerRow.append($('<div class="statuscell">').text('Status'))
|
|
|
|
if (includeActions) {
|
|
headerRow.append($('<div class="actionscell">').text('Actions'))
|
|
}
|
|
|
|
headerRow.append($('<div class="lastchangecell">').text('Last Change'))
|
|
|
|
return headerRow
|
|
}
|
|
/*
|
|
* For use in conjunction with createBranchGraphHeaders(), render a table row element for a given NodeBot and branch.
|
|
* branchData should be the specific branch object from getBranchList() (i.e. def= {...}, bot = "...", etc.)
|
|
*/
|
|
function createNodeAndEdgeRowsForBranch(branchData, includeActions, singleEdgeData=null) {
|
|
let returnArray = []
|
|
|
|
// Create node row
|
|
let nodeRow = $(`<div id="${getNodeRowId(branchData.bot, branchData.def.name)}" class="nrow">`)
|
|
|
|
// Create node row -- if we're single edge, we shouldn't include collapse controls
|
|
nodeRow.append(createNodeRow(branchData, includeActions, !!!singleEdgeData))
|
|
returnArray.push(nodeRow)
|
|
|
|
let edgeListingRow = $(`<div class="elist-row">`)
|
|
let collapseDiv = $(`<div id="${getNodeEdgesRowId(branchData.bot, branchData.def.name)}" class="${!!!singleEdgeData ? 'collapse' : '' }">`).appendTo(edgeListingRow)
|
|
let cardDiv = $('<div class="card card-body">').appendTo(collapseDiv)
|
|
// Add empty canvas to card div
|
|
$('<div class="elist-canvas">').appendTo(cardDiv)
|
|
// Create edge table
|
|
let etableDiv = $('<div class="etable">').appendTo(cardDiv)
|
|
|
|
|
|
// If we only have one edge, we should only render that edge
|
|
if (singleEdgeData) {
|
|
let singleEdgeRow = $(`<div class="erow" id="${getEdgeRowId(branchData.bot, branchData.def.name, singleEdgeData.target)}">`)
|
|
.appendTo(etableDiv)
|
|
singleEdgeRow.append(createEdgeRow(branchData, singleEdgeData, includeActions))
|
|
}
|
|
else {
|
|
Object.keys(branchData.edges).forEach( (edgeKey) => {
|
|
const edgeData = branchData.edges[edgeKey]
|
|
// If one of our edges needs attention, ensure we start out expanded
|
|
if (edgeData.blockage && !collapseDiv.hasClass('show')) {
|
|
collapseDiv.addClass('show')
|
|
}
|
|
|
|
// make a row containing:
|
|
let edgeRow = $(`<div class="erow" id="${getEdgeRowId(branchData.bot, branchData.def.name, edgeData.target)}">`).appendTo(etableDiv)
|
|
edgeRow.append(createEdgeRow(branchData, edgeData, includeActions))
|
|
})
|
|
}
|
|
|
|
// Append the finished row to the supplied table body
|
|
returnArray.push(edgeListingRow)
|
|
return returnArray
|
|
}
|
|
|
|
|
|
// Helper function to create consistent columns between nodes
|
|
function createNodeRow(nodeData, includeActions, includeCollapseControls=true) {
|
|
let columnArray = []
|
|
let conflict = null
|
|
if (nodeData.is_blocked) {
|
|
conflict = getConflict(nodeData.conflicts, nodeData.blockage.targetBranchName, nodeData.blockage.change)
|
|
}
|
|
|
|
// Branch Name Column
|
|
columnArray.push(renderNameCell_Common(nodeData, nodeData.bot))
|
|
postRenderNameCell_Node(columnArray[0], nodeData, includeCollapseControls, conflict)
|
|
|
|
// Status Column
|
|
columnArray.push(preRenderStatusCell_Node(nodeData))
|
|
renderStatusCell_Common(columnArray[1], nodeData)
|
|
|
|
// Actions
|
|
const operationArgs = [nodeData.bot, getAPIName(nodeData)]
|
|
if (includeActions) {
|
|
columnArray.push(preRenderActionsCell_Node(nodeData))
|
|
renderActionsCell_Common(columnArray[2], nodeData, nodeAPIOp, operationArgs, conflict)
|
|
}
|
|
|
|
// Last Change Column
|
|
if (window.showNodeLastChanges) {
|
|
columnArray.push(renderLastChangeCell_Common(nodeData.bot, nodeData.def.name, nodeData.last_cl, nodeData.swarmURL, nodeAPIOp, operationArgs))
|
|
}
|
|
else {
|
|
columnArray.push($('<div>').addClass('lastchangecell'))
|
|
}
|
|
|
|
return columnArray
|
|
|
|
}
|
|
|
|
// Helper function to create consistent columns between edges
|
|
function createEdgeRow(nodeData, edgeData, includeActions) {
|
|
let columnArray = []
|
|
|
|
// Branch Name Column
|
|
columnArray.push(renderNameCell_Common(edgeData, nodeData.bot))
|
|
let conflict = getConflict(nodeData.conflicts, edgeData.target, edgeData.blockage ? edgeData.blockage.change : null)
|
|
postRenderNameCell_Edge(columnArray[0], edgeData, conflict)
|
|
|
|
// Status Column
|
|
columnArray.push(preRenderStatusCell_Edge(nodeData, edgeData))
|
|
renderStatusCell_Common(columnArray[1], edgeData)
|
|
|
|
// Actions
|
|
const operationArgs = [nodeData.bot, getAPIName(nodeData), getAPIName(edgeData)]
|
|
if (includeActions) {
|
|
columnArray.push(preRenderActionsCell_Edge(edgeData))
|
|
|
|
if (!conflict && edgeData.is_blocked) {
|
|
// Our edge is blocked but our node has no conflict record for it. (This is a larger issue with RM)
|
|
// Create a mock conflict to get appropriate UI options.
|
|
conflict = {
|
|
cl: edgeData.blockage.change,
|
|
kind: 'Mock Conflict',
|
|
target: edgeData.target,
|
|
targetStream: edgeData.targetStream
|
|
}
|
|
}
|
|
|
|
renderActionsCell_Common(columnArray[2], edgeData, edgeAPIOp, operationArgs, conflict)
|
|
renderActionsCell_Edge(columnArray[2], nodeData, edgeData, conflict)
|
|
}
|
|
|
|
// Last Change Column
|
|
let catchupText = null
|
|
if (edgeData.num_changes_remaining > 1) {
|
|
catchupText = `pending: ${edgeData.num_changes_remaining}`
|
|
}
|
|
columnArray.push(renderLastChangeCell_Common(nodeData.bot, nodeData.def.name, edgeData.last_cl, edgeData.swarmURL,
|
|
edgeAPIOp,operationArgs, catchupText, edgeData.display_name))
|
|
postRenderLastChangeCell_Edge(nodeData.bot, columnArray[columnArray.length - 1], edgeData, edgeAPIOp, operationArgs)
|
|
|
|
return columnArray
|
|
}
|
|
|
|
// Helper function to create branch name column
|
|
function renderNameCell_Common(data, botname) {
|
|
let entireNameDiv = $('<div class="namecell">')
|
|
const name = getDisplayName(data)
|
|
|
|
// This 'cell' (really just a <div>) is going to contain at least one, possibly two div containers:
|
|
// 1. "topInternalDiv" - The name, status, root, and info button
|
|
// 2. If the node is blocked, it will also have a pause info div below the previous
|
|
let topInternalDiv = $('<div>').appendTo(entireNameDiv)
|
|
$('<div class="element-name">').appendTo(topInternalDiv).text(name)
|
|
|
|
// if the branch is active, display status
|
|
if (data.status_msg)
|
|
{
|
|
const statusSince = new Date(data.status_since)
|
|
$('<div class="status-msg">')
|
|
.text(data.status_msg)
|
|
.attr('title', `Since ${statusSince}`)
|
|
.appendTo(topInternalDiv)
|
|
|
|
const currentlyRunningDiv = $('div#currentlyRunning')
|
|
if (currentlyRunningDiv.length > 0) {
|
|
currentlyRunningDiv.empty()
|
|
|
|
currentlyRunningDiv.html(`<strong>${botname}:${name} Currently Running:</strong> ${data.status_msg}`)
|
|
currentlyRunningDiv.attr('title', `Since ${statusSince}`)
|
|
}
|
|
}
|
|
|
|
// Display Reconsidering text if applicable
|
|
if (data.queue && data.queue.length > 0)
|
|
{
|
|
let queueDiv = $('<div class="status-msg">').appendTo(topInternalDiv).append("Reconsidering: ");
|
|
data.queue.forEach( (queueData) => {
|
|
$('<span class="reconsidering badge badge-primary">')
|
|
.appendTo(queueDiv)
|
|
.text(`${queueData.cl}`);
|
|
})
|
|
}
|
|
|
|
if (data.is_available) {
|
|
if (data.windowBypass) {
|
|
$('<div>').addClass('status-msg warning')
|
|
.appendTo(topInternalDiv)
|
|
.text('Bypassing window!');
|
|
}
|
|
}
|
|
else {
|
|
const unpauseTime = data.blockage && data.blockage.endsAt ? new Date(data.blockage.endsAt) : 'never';
|
|
let msg = '';
|
|
|
|
if (data.is_paused) {
|
|
msg += `Paused by ${data.manual_pause.owner}, please contact before unpausing.<br />`;
|
|
}
|
|
if (data.is_blocked) {
|
|
if (data.blockage.acknowledger) {
|
|
msg += `Acknowledged by ${data.blockage.acknowledger}, please contact for more information.<br />`;
|
|
} else {
|
|
owner = data.blockage.owner
|
|
ownerMatch = owner?.match(/<!.*\|(.*)>/)
|
|
if (ownerMatch) {
|
|
owner = `@${ownerMatch[1]}`
|
|
}
|
|
msg += `Blocked but not acknowledged. Possible owner: ${owner}<br />`;
|
|
}
|
|
}
|
|
|
|
if (data.is_paused || data.is_blocked) {
|
|
msg += `Will unpause and retry on: ${unpauseTime}.<br />`;
|
|
}
|
|
|
|
if (msg) {
|
|
$('<div class="status-msg">')
|
|
.appendTo(topInternalDiv)
|
|
.html($('<p>').html(msg));
|
|
}
|
|
}
|
|
|
|
// Appending the P4 Rootpath floating to the right side
|
|
let rootPathStr = data.def ? data.def.rootPath :
|
|
data.rootPath ? data.rootPath :
|
|
null
|
|
|
|
if (rootPathStr) {
|
|
let rootPath = $('<div class="rootpath">')
|
|
.text(rootPathStr)
|
|
.appendTo(topInternalDiv)
|
|
|
|
// Append info button to branch name
|
|
//if (data.def.isMonitored) {
|
|
$('<button class="btn btn-xs btn-primary-alt configinfo"><i class="fas fa-info-circle"></i></button>')
|
|
.appendTo(rootPath)
|
|
.click(function() {
|
|
alert(JSON.stringify(data, null, ' '));
|
|
});
|
|
//}
|
|
}
|
|
|
|
return entireNameDiv
|
|
}
|
|
function createPauseDivs(data, conflict) {
|
|
const divs = []
|
|
|
|
// Display pause data if applicable
|
|
if (data.is_paused) {
|
|
divs.push($('<div class="info-block pause">')
|
|
.append(
|
|
$('<p>').html(`<h6>Paused by ${data.manual_pause.owner}</h6><span class="pause-div-label">On:</span> ${new Date(data.manual_pause.startedAt).toString()}<br><span class="pause-div-label">Reason:</span> ${data.manual_pause.message}`)
|
|
)
|
|
)
|
|
}
|
|
|
|
// Display blockage data if applicable
|
|
if (data.is_blocked) {
|
|
const changelist = conflict && conflict.cl ? conflict.cl :
|
|
data.blockage && data.blockage.change ? data.blockage.change :
|
|
'unknown'
|
|
|
|
let info = conflict && conflict.kind ? `<span class="pause-div-label">Cause:</span> <strong>${conflict.kind.toLowerCase()}</strong>` :
|
|
data.blockage ? `<span class="pause-div-label">Blocked.</span> Type: ${data.blockage.type}<br /> Message: ${data.blockage.message}` :
|
|
`No info can be provided. Please contact Robomerge help.`
|
|
|
|
if (conflict && conflict.slackLinks) {
|
|
info += `<br>${conflict.slackLinks.map(link => `<a href="${link}" target="_blank">Slack Thread</a>`).join("<br>")}`
|
|
}
|
|
|
|
divs.push($('<div class="info-block conflict">')
|
|
.append(
|
|
$('<p>').html('<h6>Blocked on change ' + makeClLink(changelist, data.swarmURL) + `</h6> ${info}`)
|
|
)
|
|
)
|
|
}
|
|
|
|
return divs
|
|
}
|
|
function postRenderNameCell_Node(nameCell, nodeData, includeCollapseControls, conflict=null) {
|
|
// For nodes, go through the edges to display a badge next to the name for blocked and paused edges
|
|
let edgesInConflictCount = 0
|
|
let edgesPausedCount = 0
|
|
const edgeNames = Object.keys(nodeData.edges)
|
|
if (nodeData.edges) {
|
|
edgeNames.forEach( (edgeKey) => {
|
|
const edgeData = nodeData.edges[edgeKey]
|
|
if (edgeData.is_blocked || edgeData.retry_cl > 0) {
|
|
edgesInConflictCount++
|
|
}
|
|
else if (edgeData.is_paused) {
|
|
edgesPausedCount++
|
|
}
|
|
})
|
|
}
|
|
|
|
const totalBlockages = edgesInConflictCount + !!nodeData.is_blocked
|
|
|
|
let badges = []
|
|
if (totalBlockages > 0) {
|
|
|
|
if (edgesInConflictCount === edgeNames.length) {
|
|
badges.push($('<div>')
|
|
.html('<i class="fas fa-exclamation-triangle"></i>')
|
|
.attr('title', 'ALL EDGES BLOCKED')
|
|
.css('color', 'DARKRED')
|
|
)
|
|
}
|
|
|
|
badges.push($('<span class="badge badge-pill badge-danger" title="Edges Conflicted">').text(totalBlockages))
|
|
}
|
|
if (edgesPausedCount > 0) {
|
|
badges.push($('<span class="badge badge-pill badge-warning" title="Edges Paused">').text(edgesPausedCount))
|
|
}
|
|
|
|
const nameHeader = nameCell.find('div.element-name').first()
|
|
let collapseLink = null
|
|
|
|
if (includeCollapseControls && nodeData.edges && Object.keys(nodeData.edges).length !== 0) {
|
|
// Replace the context with collapse controls
|
|
nameHeader.empty()
|
|
|
|
const name = getDisplayName(nodeData)
|
|
collapseLink = $('<a role="button" data-toggle="collapse" title="Show/Hide Edges">').appendTo(nameHeader)
|
|
collapseLink.attr('data-target', '#' + getNodeEdgesRowId(nodeData.bot, name))
|
|
|
|
let icon = $('<i class="fas fa-lg">').appendTo(collapseLink)
|
|
|
|
// If we have any edges in conflict, we default to showing the edge table. Display the caret down
|
|
if (edgesInConflictCount > 0) {
|
|
icon.addClass('fa-caret-down')
|
|
} else {
|
|
icon.addClass('fa-caret-right') // Collapsed
|
|
}
|
|
|
|
collapseLink.append(` ${name}`, badges)
|
|
}
|
|
else {
|
|
nameHeader.append(badges)
|
|
}
|
|
|
|
// Append Pause Info
|
|
if (!nodeData.is_available) {
|
|
nameCell.append(createPauseDivs(nodeData, conflict))
|
|
}
|
|
|
|
// We need to represent if edges need attention
|
|
if (nodeData.conflicts && nodeData.edges
|
|
&& Array.isArray(nodeData.conflicts) && Array.isArray(nodeData.edges)
|
|
&& nodeData.conflicts.length >= Object.keys(nodeData.edges).length) {
|
|
|
|
// Alert if all edges are blocked by putting a danger icon beside the name
|
|
if (collapseLink) {
|
|
collapseLink.append(dangerSymbol)
|
|
}
|
|
else {
|
|
nameHeader
|
|
}
|
|
}
|
|
}
|
|
function postRenderNameCell_Edge(nameCell, edgeData, conflict=null) {
|
|
// Append Pause Info
|
|
if (!edgeData.is_available) {
|
|
nameCell.append(createPauseDivs(edgeData, conflict))
|
|
}
|
|
}
|
|
|
|
// Helper function to create status data cell
|
|
function preRenderStatusCell_Node(nodeData) {
|
|
// Create holder for the stylized status text
|
|
let statusCell = $('<div class="statuscell">')
|
|
|
|
if (!nodeData.def.isMonitored) {
|
|
statusCell.addClass("unmonitored-text")
|
|
statusCell.text('Unmonitored')
|
|
}
|
|
|
|
if (nodeData.is_blocked) {
|
|
$('<div class="blockageinfo">')
|
|
.text('ENTIRELY BLOCKED')
|
|
.addClass('important-status')
|
|
.css('color', 'DARKRED')
|
|
.appendTo(statusCell)
|
|
}
|
|
|
|
return statusCell
|
|
}
|
|
function preRenderStatusCell_Edge(nodeData, edgeData) {
|
|
// Create holder for the stylized status text
|
|
let statusCell = $('<div>').addClass('statuscell')
|
|
|
|
if (edgeData.is_blocked) {
|
|
$('<div class="blockageinfo">')
|
|
.text('BLOCKED')
|
|
.addClass('important-status')
|
|
.css('color', 'DARKRED')
|
|
.appendTo(statusCell)
|
|
}
|
|
else if (!edgeData.is_paused) {
|
|
// If we're not paused, but the node is paused or blocked, display a message informing users
|
|
// that we'll not be receiving any work.
|
|
if (nodeData.is_paused || nodeData.is_blocked || nodeData.retry_cl) {
|
|
$('<div>')
|
|
.text('NODE ' + (nodeData.is_paused ? 'PAUSED' : 'BLOCKED'))
|
|
.addClass('important-status')
|
|
.css('color', 'ORANGE')
|
|
.prependTo(statusCell)
|
|
}
|
|
else if (edgeData.gateClosedMessage) {
|
|
let msg = edgeData.gateClosedMessage
|
|
if (edgeData.nextWindowOpenTime) {
|
|
const timestamp = Date.parse(edgeData.nextWindowOpenTime)
|
|
if (!isNaN(timestamp)) {
|
|
msg += ` (opens: ${(new Date(timestamp)).toLocaleString()})`
|
|
}
|
|
}
|
|
let statusMsg = edgeData.waitingForCISLink ? $(`<a href="${edgeData.waitingForCISLink}">`).prop('target', '_blank') : $("<div>")
|
|
statusMsg.addClass('status-msg').text(msg).appendTo(statusCell)
|
|
}
|
|
}
|
|
|
|
return statusCell
|
|
}
|
|
function renderStatusCell_Common(statusCell, data) {
|
|
// Don't bother with unmonitored nodes
|
|
if (statusCell.hasClass('unmonitored-text')) {
|
|
return
|
|
}
|
|
|
|
if (data.is_paused) {
|
|
let pauseInfo = $('<div>')
|
|
.text('PAUSED')
|
|
.addClass('important-status')
|
|
.css('color', 'ORANGE')
|
|
.prependTo(statusCell)
|
|
|
|
let [pausedDurationStr, pausedDurationColor] = printDurationInfo('Paused',
|
|
Date.now() - new Date(data.manual_pause.startedAt).getTime())
|
|
$('<div class="pause-details">')
|
|
.html(`${pausedDurationStr} by <strong>${data.manual_pause.owner}</strong>`)
|
|
.css('color', pausedDurationColor)
|
|
.insertAfter(pauseInfo)
|
|
}
|
|
|
|
if (data.is_blocked) {
|
|
const blockageinfoDiv = statusCell.find('.blockageinfo')
|
|
// If we have an acknowledged date, print acknowledged text
|
|
if (data.blockage.acknowledgedAt) {
|
|
let acknowledgedSince = new Date(data.blockage.acknowledgedAt);
|
|
let [ackDurationStr, ackDurationColor] = printDurationInfo("Acknowledged", Date.now() - acknowledgedSince.getTime())
|
|
$('<div class="blockage-details">')
|
|
.css('color', ackDurationColor)
|
|
.html(`${ackDurationStr} by <strong>${data.blockage.acknowledger}</strong>`)
|
|
.insertAfter(blockageinfoDiv)
|
|
}
|
|
// Determine who is responsible for resolving this.
|
|
else {
|
|
let blockedSince = new Date(data.blockage.startedAt)
|
|
let [blockedDurationStr, blockedDurationColor] = printDurationInfo("Blocked", Date.now() - blockedSince.getTime())
|
|
const pauseCulprit = data.blockage.owner || data.blockage.author || "unknown culprit";
|
|
|
|
const blockageDetailsDiv = $('<div class="blockage-details">').insertAfter(blockageinfoDiv)
|
|
|
|
$('<div>')
|
|
.css('color', blockedDurationColor)
|
|
.html(blockedDurationStr)
|
|
.appendTo(blockageDetailsDiv)
|
|
|
|
$('<div>')
|
|
.html(`Resolver: <strong>${pauseCulprit} (unacknowledged)</strong>`)
|
|
.appendTo(blockageDetailsDiv)
|
|
}
|
|
}
|
|
|
|
let text = null, color = 'GREEN'
|
|
// If the node is currently processing (active) display as such
|
|
if (data.retry_cl) {
|
|
text = 'RETRYING'
|
|
color = 'ORANGE'
|
|
}
|
|
else if (data.is_active) {
|
|
text = 'ACTIVE'
|
|
}
|
|
// Otherwise, if we're available and we haven't populated any data inside statusCell, print that we're running
|
|
else if (data.is_available && statusCell.children().length === 0) {
|
|
text = 'RUNNING'
|
|
}
|
|
|
|
if (text) {
|
|
$('<div>')
|
|
.text(text)
|
|
.css('color', color)
|
|
.prependTo(statusCell)
|
|
}
|
|
|
|
return statusCell
|
|
}
|
|
|
|
// Helper function to create actions cell
|
|
function preRenderActionsCell_Node(nodeData) {
|
|
let actionCell = $('<div class="actionscell">')
|
|
|
|
// If the branch is not monitored, we can't provide actions. Simply provide text response and return.
|
|
if (!nodeData.def.isMonitored) {
|
|
actionCell.append($('<div class="unactionable-text">').text("No Actions Available"))
|
|
actionCell.addClass('unactionable')
|
|
}
|
|
else if (!nodeData.edges) {
|
|
actionCell.append($('<div class="unactionable-text">').text("No Edges Configured"))
|
|
actionCell.addClass('unactionable')
|
|
}
|
|
|
|
return actionCell
|
|
}
|
|
function preRenderActionsCell_Edge(_edgeData) {
|
|
return $('<div class="actionscell">')
|
|
}
|
|
// operationArgs should be a string array with these elements:
|
|
// 0 - bot name
|
|
// 1 - branch/node name
|
|
// 2 - edge name (if applicable)
|
|
function renderActionsCell_Common(actionCell, data, operationFunction, operationArgs, conflict=null) {
|
|
// Don't bother with unmonitored nodes
|
|
if (actionCell.hasClass('unactionable-text')) {
|
|
return
|
|
}
|
|
const botname = operationArgs[0]
|
|
const branchNameForAPI = operationArgs[1]
|
|
const optEdgeName = operationArgs[2]
|
|
const dataDisplayName = getDisplayName(data)
|
|
const dataFullName = `${botname}:${dataDisplayName}`
|
|
|
|
// Start collecting available actions in our button group
|
|
|
|
|
|
|
|
// Keep track if we have an individual action button forming our default option for a given NodeBot ---
|
|
// this controls how we render the dropdown menu
|
|
let specificActionButton = null
|
|
// If manually paused, the default option should be to unpause it
|
|
if (data.is_paused) {
|
|
specificActionButton = createActionButton(
|
|
`Unpause`,
|
|
function() {
|
|
let unpauseConfirmation = confirm(`Are you sure you want to unpause ${dataDisplayName}?`)
|
|
if (unpauseConfirmation) {
|
|
operationFunction(...operationArgs, '/unpause', function(success) {
|
|
if (success) {
|
|
displaySuccessfulMessage(`Unpaused ${dataDisplayName}.`)
|
|
} else {
|
|
displayErrorMessage(`Error unpausing ${dataFullName}, please check logs.`)
|
|
$('#helpButton').trigger('click')
|
|
}
|
|
updateBranchList(botname)
|
|
})
|
|
}
|
|
},
|
|
`Resume Robomerge monitoring of ${dataDisplayName}`
|
|
)
|
|
}
|
|
// If the whole node is paused on a blocker, the acknowledge functions should be the default options
|
|
else if (data.is_blocked) {
|
|
let ackQueryData = toQuery({
|
|
cl: data.blockage.change
|
|
})
|
|
|
|
let ackFunc = function() {
|
|
operationFunction(...operationArgs, '/acknowledge?' + ackQueryData,
|
|
function(success) {
|
|
displaySuccessfulMessage(`Acknowledged blockage in ${dataFullName}.`)
|
|
updateBranchList(botname)
|
|
},
|
|
function(failure) {
|
|
displayErrorMessage(`Error acknowledging blockage in ${dataFullName}, please contact Robomerge support. Error message: ${failure}`)
|
|
$('#helpButton').trigger('click')
|
|
updateBranchList(botname)
|
|
}
|
|
)
|
|
}
|
|
let unackFunc = function() {
|
|
operationFunction(...operationArgs, '/unacknowledge?' + ackQueryData,
|
|
function(success) {
|
|
displaySuccessfulMessage(`Unacknowledged blockage in ${dataFullName}.`)
|
|
updateBranchList(botname)
|
|
},
|
|
function(failure) {
|
|
displayErrorMessage(`Error unacknowledging blockage in ${dataFullName}, please contact Robomerge support. Error message: ${failure}`)
|
|
$('#helpButton').trigger('click')
|
|
updateBranchList(botname)
|
|
}
|
|
)
|
|
}
|
|
|
|
// Acknowledge
|
|
if (!data.blockage.acknowledger) {
|
|
specificActionButton = createActionButton(
|
|
[$('<i class="fas fa-exclamation-triangle" style="color:yellow">'), ' ', "Acknowledge"],
|
|
ackFunc,
|
|
'Take ownership of this blockage'
|
|
)
|
|
}
|
|
// Unacknowledge
|
|
else if (robomergeUser && data.blockage.acknowledger === robomergeUser.userName) {
|
|
specificActionButton = createActionButton(
|
|
[$('<i class="fas fa-eject" style="color:red">'), ' ', "Unacknowledge"],
|
|
unackFunc,
|
|
'Back-pedal: someone else should fix this'
|
|
)
|
|
}
|
|
// Take ownership
|
|
else {
|
|
specificActionButton = createActionButton(
|
|
[$('<i class="fas fa-hands-helping" style="color:white">'), ' ', "Take Ownership"],
|
|
ackFunc,
|
|
`Take ownership of this blockage over ${data.blockage.acknowledger}`
|
|
)
|
|
}
|
|
}
|
|
|
|
// collect actions that should be added to a drop-down, if any
|
|
const dropdownEntries = []
|
|
if (data.is_blocked) {
|
|
// If this object has a conflict, report it
|
|
if (conflict) {
|
|
const queryParams = toQuery({
|
|
bot: botname,
|
|
branch: branchNameForAPI,
|
|
target: conflict.target,
|
|
cl: conflict.cl
|
|
})
|
|
|
|
// Create Shelf
|
|
let shelfRequest = '/op/createshelf?' + queryParams
|
|
if (conflict.targetStream) {
|
|
shelfRequest += '&' + toQuery({ targetStream: conflict.targetStream })
|
|
}
|
|
shelfRequest += location.hash
|
|
|
|
const toTargetText = conflict.target ? ' -> ' + conflict.target : '';
|
|
const shelfOption = createActionOption('Create Shelf for ' + conflict.cl + toTargetText, function() {
|
|
window.location.href = shelfRequest;
|
|
}, `Retry merge of ${conflict.cl} and create a shelf in a specified P4 workspace`)
|
|
|
|
// Stomp
|
|
const stompRequest = '/op/stomp?' + queryParams + location.hash
|
|
const stompOption = createActionOption('Stomp Changes using ' + conflict.cl + toTargetText, function() {
|
|
window.location.href = stompRequest;
|
|
}, `Use ${conflict.cl} to stomp binary changes in ${conflict.target}`);
|
|
|
|
// Unlock files
|
|
const unlockFilesRequest = '/op/unlock?' + queryParams
|
|
const unlockOption = createActionOption('Unlock Files blocking ' + conflict.cl + toTargetText, function() {
|
|
window.location.href = unlockFilesRequest;
|
|
}, `Unlock files blocking ${conflict.cl} in ${conflict.target}`);
|
|
|
|
// Can only create shelves for merge conflicts and commit failures
|
|
if (conflict.kind !== 'Merge conflict' &&
|
|
conflict.kind !== 'Commit failure' &&
|
|
conflict.kind !== 'Transfer error') {
|
|
shelfOption.addClass("disabled")
|
|
shelfOption.off('click')
|
|
// Bootstrap magic converts 'title' to 'data-original-title'
|
|
shelfOption.attr('data-original-title', `Shelving not available for ${conflict.kind.toLowerCase()}.`)
|
|
}
|
|
|
|
dropdownEntries.push(shelfOption)
|
|
|
|
// Can only perform stomps for merge conflicts
|
|
if (conflict.kind === 'Merge conflict') {
|
|
dropdownEntries.push(stompOption)
|
|
}
|
|
else {
|
|
stompOption.addClass("disabled")
|
|
stompOption.off('click')
|
|
stompOption.attr('data-original-title', `Stomp not available for ${conflict.kind.toLowerCase()}.`)
|
|
}
|
|
|
|
if (conflict.kind === 'Exclusive check-out') {
|
|
dropdownEntries.push(unlockOption)
|
|
}
|
|
else {
|
|
unlockOption.addClass("disabled")
|
|
unlockOption.off('click')
|
|
unlockOption.attr('data-original-title', `Unlock not required for ${conflict.kind.toLowerCase()}.`)
|
|
}
|
|
|
|
}
|
|
|
|
// Exposing this section in the case of a blockage but no conflict
|
|
// Retry CL #
|
|
const retryOption = createActionOption(`Retry ${data.blockage.change}`, function() {
|
|
operationFunction(...operationArgs, '/retry', function(success) {
|
|
if (success) {
|
|
displaySuccessfulMessage(`Resuming ${dataDisplayName} and retrying ${data.blockage.change}`)
|
|
} else {
|
|
displayErrorMessage(`Error resuming ${dataDisplayName} and retrying ${data.blockage.change}, please check logs.`)
|
|
$('#helpButton').trigger('click')
|
|
}
|
|
updateBranchList(botname)
|
|
})
|
|
}, `Resume Robomerge monitoring of ${dataDisplayName} and retry ${data.blockage.change}`)
|
|
dropdownEntries.push(retryOption)
|
|
|
|
dropdownEntries.push('<div class="dropdown-divider">')
|
|
}
|
|
|
|
if (!data.is_paused) {
|
|
// Pause Option
|
|
const pauseOption = createActionOption(`Pause ${dataDisplayName}`, function() {
|
|
let pauseReasonPrompt = promptFor({
|
|
msg: `Why are you pausing ${dataDisplayName}?`}
|
|
);
|
|
if (pauseReasonPrompt) {
|
|
pauseReason = pauseReasonPrompt.msg.toString().trim()
|
|
// Request a reason.
|
|
if (pauseReason === "") {
|
|
displayErrorMessage(`Please provide reason for pausing ${dataFullName}. Cancelling pause request.`)
|
|
return
|
|
}
|
|
|
|
operationFunction(...operationArgs, "/pause?" + toQuery(pauseReasonPrompt), function(success) {
|
|
if (success) {
|
|
displaySuccessfulMessage(`Paused ${dataDisplayName}: "${pauseReasonPrompt.msg}"`)
|
|
} else {
|
|
displayErrorMessage(`Error pausing ${dataDisplayName}, please check logs.`)
|
|
$('#helpButton').trigger('click')
|
|
}
|
|
updateBranchList(botname)
|
|
})
|
|
}
|
|
}, `Pause Robomerge monitoring of ${dataDisplayName}`)
|
|
dropdownEntries.push(pauseOption)
|
|
}
|
|
|
|
if (optEdgeName || window.allowNodeReconsiders) {
|
|
// Add Reconsider action
|
|
const reconsiderOption = createActionOption(`Reconsider...`, function() {
|
|
let data = promptFor({
|
|
cl: `Enter the CL to reconsider (should be a CL from ${dataDisplayName}):`
|
|
});
|
|
if (data) {
|
|
// Ensure they entered a CL
|
|
if (isNaN(parseInt(data.cl))) {
|
|
displayErrorMessage("Please provide a valid changelist number to reconsider.")
|
|
return
|
|
}
|
|
|
|
operationFunction(...operationArgs, "/reconsider?" + toQuery(data), function(success) {
|
|
if (success) {
|
|
displaySuccessfulMessage(`Reconsidering ${data.cl} in ${dataFullName}...`)
|
|
} else {
|
|
displayErrorMessage(`Error reconsidering ${data.cl}, please check logs.`)
|
|
$('#helpButton').trigger('click')
|
|
}
|
|
updateBranchList(botname)
|
|
});
|
|
}
|
|
}, `Manually submit a CL to Robomerge to process (should be a CL from ${dataDisplayName})`)
|
|
dropdownEntries.push(reconsiderOption)
|
|
}
|
|
|
|
// if no available actions, we're done
|
|
if (!specificActionButton && dropdownEntries.length === 0) {
|
|
return
|
|
}
|
|
|
|
const buttonGroup = $('<div class="btn-group">').appendTo(actionCell)
|
|
// Ensure we don't do a refresh of the branch list while displaying a dropdown
|
|
buttonGroup.on('show.bs.dropdown', function() {
|
|
// Check to see if we have our mutex function
|
|
if (typeof pauseAutoRefresh === 'function') {
|
|
pauseAutoRefresh()
|
|
}
|
|
})
|
|
buttonGroup.on('hidden.bs.dropdown', function() {
|
|
// Check to see if we have our mutex function
|
|
if (typeof resumeAutoRefresh === 'function') {
|
|
resumeAutoRefresh()
|
|
}
|
|
})
|
|
|
|
if (specificActionButton) {
|
|
buttonGroup.append(specificActionButton)
|
|
}
|
|
|
|
if (dropdownEntries.length > 0) {
|
|
// Create dropdown menu button
|
|
const actionsDropdownButton = $('<button class="btn dropdown-toggle" data-toggle="dropdown">')
|
|
// If there is already a button, we will append this no-text button to the end of it
|
|
|
|
if (specificActionButton) {
|
|
actionsDropdownButton.addClass('btn-primary-alt') // This is our own take on the primary button
|
|
actionsDropdownButton.addClass('dropdown-toggle-split')
|
|
}
|
|
else {
|
|
// If we have no conflicts, simply label our non-emergency actions
|
|
actionsDropdownButton.text('Actions\n')
|
|
actionsDropdownButton.addClass('btn btn-secondary-alt') // This is our own take on the secondary button
|
|
}
|
|
actionsDropdownButton.appendTo(buttonGroup)
|
|
|
|
const optionsList = $('<div class="dropdown-menu">').appendTo(buttonGroup)
|
|
for (const entry of dropdownEntries) {
|
|
optionsList.append(entry)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
function renderActionsCell_Edge(actionCell, nodeData, edgeData, conflict=null) {
|
|
const edgeName = getDisplayName(edgeData)
|
|
const optionsList = $(actionCell).find(".btn-group>.dropdown-menu")
|
|
|
|
if (optionsList.length !== 1) {
|
|
error('Could not complete rendering action cell for edge ' + edgeName + '.')
|
|
return
|
|
}
|
|
|
|
const insertFunction = function(element) {
|
|
// Try to insert the skip option as the last contextual operation, signified by the dropdown divider
|
|
let divider = optionsList.find('.dropdown-divider')
|
|
if (divider.length !== 1) {
|
|
// If we for some reason can't find the divider, just append
|
|
optionsList.append(element)
|
|
}
|
|
else {
|
|
element.insertBefore(divider)
|
|
}
|
|
}
|
|
|
|
if (edgeData.is_blocked && edgeData.blockage && edgeData.blockage.targetBranchName) {
|
|
// Skip CL
|
|
const skipEnabled = !edgeData.disallowSkip
|
|
|
|
const skipChangelistText = `Skip Changelist ${edgeData.blockage.change}`
|
|
if (skipEnabled) {
|
|
let skipRequest = '/op/skip?' + toQuery({
|
|
bot: nodeData.bot,
|
|
branch: getAPIName(nodeData),
|
|
cl: edgeData.blockage.change,
|
|
edge: edgeData.blockage.targetBranchName
|
|
}) + location.hash
|
|
|
|
const tooltip = `Skip past the blockage caused by changelist ${edgeData.blockage.change}. `
|
|
+ "This option should only be selected if the work does not need to be merged or you will merge this work youself."
|
|
const skipOption = createActionOption(skipChangelistText, function() {
|
|
window.location.href = skipRequest;
|
|
}, tooltip)
|
|
insertFunction(skipOption)
|
|
}
|
|
else {
|
|
|
|
insertFunction(createActionOption(skipChangelistText, null,
|
|
`Skip has been disabled.`).addClass("disabled").off('click')
|
|
)
|
|
}
|
|
}
|
|
|
|
if (edgeData.nextWindowOpenTime) {
|
|
const sense = edgeData.windowBypass ? 'false' : 'true'
|
|
|
|
const text = edgeData.windowBypass
|
|
? 'Re-enable gate window'
|
|
: 'Bypass gate window';
|
|
|
|
const tooltip = edgeData.windowBypass
|
|
? 'Integrations will be delayed until window is open'
|
|
: 'Integrations will proceed, disregarding specified window';
|
|
|
|
const bypassRequest = () =>
|
|
edgeAPIOp(nodeData.bot, getAPIName(nodeData), edgeData.target, '/bypassgatewindow?sense=' + sense, () => {}, () => {});
|
|
|
|
insertFunction(createActionOption(text, bypassRequest, tooltip));
|
|
}
|
|
}
|
|
|
|
// Helper function to create last change cell
|
|
function renderLastChangeCell_Common(botname, nodename, lastCl, swarmURL, operationFunction, operationArgs, catchupText=null, edgename=null) {
|
|
const lastChangeCell = $('<div>').addClass('lastchangecell')
|
|
|
|
const swarmLink = $(makeClLink(lastCl, swarmURL)).appendTo(lastChangeCell)
|
|
if (catchupText) {
|
|
lastChangeCell.append($('<div>').addClass('catchup').text(catchupText))
|
|
}
|
|
|
|
// On shift+click, we can set the CL instead
|
|
swarmLink.click(function(evt) {
|
|
if (evt.shiftKey)
|
|
{
|
|
let data = promptFor({
|
|
cl: {prompt: 'Enter CL', default: lastCl},
|
|
})
|
|
if (data) {
|
|
data.reason = "manually set through Robomerge homepage"
|
|
|
|
operationFunction(...operationArgs, "/set_last_cl?" + toQuery(data), function(success) {
|
|
if (success) {
|
|
updateBranchList(botname)
|
|
displaySuccessfulMessage(`Successfully set ${edgename || nodename} to changelist ${data.cl}`)
|
|
} else {
|
|
displayErrorMessage(`Error setting ${edgename || nodename} to changelist ${data.cl}, please check logs.`)
|
|
$('#helpButton').trigger('click')
|
|
}
|
|
})
|
|
|
|
}
|
|
if (evt.preventDefault) {
|
|
evt.preventDefault()
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
// shift+click end
|
|
|
|
return lastChangeCell
|
|
}
|
|
|
|
function prettyDate(date) {
|
|
try {
|
|
/*
|
|
Warning: toLocaleDateString seems to have very different behavior between browsers.
|
|
{dateStyle: "medium",timeStyle: "long"} works fine in Chrome, but not in Firefox.
|
|
When changing this, please test browser compatibility.
|
|
*/
|
|
return date.toLocaleDateString(undefined, {
|
|
month: "short",
|
|
day: "2-digit",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
timeZoneName: "short"
|
|
})
|
|
}
|
|
catch (e) {
|
|
return date.toString()
|
|
}
|
|
}
|
|
|
|
function postRenderLastChangeCell_Edge(botname, lastChangeCell, edgeData, operationFunction, operationArgs) {
|
|
if (edgeData.lastGoodCL) {
|
|
let tooltip = edgeData.lastGoodCLJobLink ? 'CL approved by CIS' : 'Paused at CL'
|
|
if (edgeData.lastGoodCLDate) {
|
|
tooltip += ` submitted ${prettyDate(new Date(edgeData.lastGoodCLDate))}`
|
|
}
|
|
if (edgeData.headCL) {
|
|
tooltip += ` (head changelist ${edgeData.headCL})`
|
|
}
|
|
|
|
let goodCL = edgeData.lastGoodCLJobLink ? $(`<a href="${edgeData.lastGoodCLJobLink}">`).prop('target', '_blank') : $('<div>');
|
|
goodCL.html('\u{2713} ' + edgeData.lastGoodCL).addClass('last-good-cl').prop('title', tooltip).appendTo(lastChangeCell)
|
|
|
|
if (!edgeData.lastGoodCLJobLink)
|
|
{
|
|
// On shift+click, we can set the CL instead
|
|
goodCL.click(function(evt) {
|
|
if (evt.shiftKey)
|
|
{
|
|
let data = promptFor({
|
|
cl: {prompt: 'Enter CL', default: edgeData.lastGoodCL},
|
|
})
|
|
if (data) {
|
|
data.reason = "manually set through Robomerge homepage"
|
|
|
|
operationFunction(...operationArgs, "/set_gate_cl?" + toQuery(data), function(success) {
|
|
if (success) {
|
|
updateBranchList(botname)
|
|
displaySuccessfulMessage(`Successfully set gate for ${edgeData.displayName} to changelist ${data.cl}`)
|
|
} else {
|
|
displayErrorMessage(`Error setting gate for ${edgeData.displayName} to changelist ${data.cl}, please check logs.`)
|
|
}
|
|
})
|
|
|
|
}
|
|
if (evt.preventDefault) {
|
|
evt.preventDefault()
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Helper functions to wrap createBranchGraphHeaders() and appendBranchGraphRow() and construct a table
|
|
function renderSingleBranchTable(branchData, singleEdgeData) {
|
|
let graphTable = createGraphTableDiv()
|
|
graphTable.append(createGraphTableHeaders(false))
|
|
graphTable.append(createNodeAndEdgeRowsForBranch(branchData, false, singleEdgeData))
|
|
return graphTable
|
|
}
|
|
|
|
function drawCanvas(edgeTableRow) {
|
|
const canvasDiv = $(edgeTableRow).find(".elist-canvas");
|
|
const canvasWidth = canvasDiv.outerWidth(true);
|
|
|
|
// If the div is already collapsed, briefly expand the div so we can accurately draw the canvas
|
|
const collapsingDiv = $(edgeTableRow).children(":first");
|
|
let startExpanded = $(collapsingDiv).hasClass("show");
|
|
let collapsingDivClassesBefore = $(collapsingDiv).attr('class')
|
|
if (!startExpanded) {
|
|
$(collapsingDiv).addClass('show');
|
|
}
|
|
// Try to find an existing canvas element first. Otherwise create a new one
|
|
let canvas = canvasDiv.find("canvas");
|
|
if (canvas.length === 0) {
|
|
canvas = $("<canvas>").appendTo(canvasDiv);
|
|
}
|
|
|
|
// Reset the canvas dimensions on re-draw before we get the div's height.
|
|
// This is to ensure zooms don't ever increase the canvas size
|
|
canvas.attr("width", 0);
|
|
canvas.attr("height", 0);
|
|
const canvasHeight = $(collapsingDiv).find("div.etable").first().outerHeight(true);
|
|
|
|
canvas.attr("width", canvasWidth);
|
|
canvas.attr("height", canvasHeight);
|
|
const context = canvas[0].getContext("2d");
|
|
|
|
if (canvasHeight > 0) {
|
|
// set element to be same size as canvas to prevent scaling
|
|
canvas.height(canvasHeight)
|
|
}
|
|
|
|
// First begin by clearing out the canvas. This comes in handy when we need to redraw the element
|
|
context.clearRect(0, 0, canvasWidth, canvasHeight);
|
|
context.fillStyle = $(edgeTableRow).find('.erow').css('background-color')
|
|
context.fillRect(0, 0, canvasWidth, canvasHeight)
|
|
|
|
// Configure line here
|
|
context.lineWidth = 2;
|
|
context.lineJoin = "miter";
|
|
context.shadowBlur = 1;
|
|
context.shadowColor = "DimGray";
|
|
|
|
// For each edge, we want to draw a line between the top of the parent row (bottom of the node row) to the edge
|
|
let previousY = 0;
|
|
const edgeRows = $(edgeTableRow).find(".erow");
|
|
edgeRows.each(function(_eRowIndex, edgeRow) {
|
|
const edgeRowHeight = $(edgeRow).outerHeight(true);
|
|
const edgeRowPos = $(edgeRow).position();
|
|
const targetY = edgeRowPos.top + edgeRowHeight / 2;
|
|
|
|
// Move to the center of the div on the X access, and start where we last left off
|
|
context.moveTo(canvasWidth / 2, previousY);
|
|
|
|
// Draw to the center of the edge row
|
|
context.lineTo(canvasWidth / 2, targetY);
|
|
|
|
// Draw over to the row
|
|
context.lineTo(canvasWidth - 5, targetY);
|
|
|
|
// Draw a short end to the line
|
|
context.moveTo(canvasWidth - 10, targetY - 5);
|
|
context.lineTo(canvasWidth - 5, targetY);
|
|
context.lineTo(canvasWidth - 10, targetY + 5);
|
|
|
|
// Finally set the previous Y to our completed line
|
|
previousY = targetY;
|
|
});
|
|
|
|
// Draw
|
|
context.stroke();
|
|
|
|
// If the div was initially collapsed, restore the state
|
|
if (!startExpanded) {
|
|
$(collapsingDiv).attr("class", collapsingDivClassesBefore);
|
|
}
|
|
}
|
|
|
|
function drawAllCanvases(graphTable) {
|
|
// Process each canvas by first finding the Edge Table Row parent
|
|
$('.gtable .elist-row').each(function(_index, edgeTableRow) {
|
|
drawCanvas(edgeTableRow);
|
|
})
|
|
}
|
|
|
|
/******************************
|
|
* END BRANCH TABLE FUNCTIONS *
|
|
******************************/
|
|
|
|
function preprocessData(data) {
|
|
|
|
for (const node of data.branches) {
|
|
const conflicts = node.conflicts
|
|
if (conflicts.length === 0) {
|
|
continue
|
|
}
|
|
|
|
const edges = node.edges
|
|
for (const edgeName of Object.keys(edges)) {
|
|
const edge = edges[edgeName]
|
|
if (!edge.is_blocked && edge.lastBlockage) {
|
|
if (conflicts.find(c => c.cl === edge.lastBlockage && c.target === edge.target.toUpperCase())) {
|
|
edge.retry_cl = edge.lastBlockage
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!node.is_blocked && node.lastBlockage) {
|
|
if (conflicts.find(c => c.cl === node.lastBlockage && !c.target)) {
|
|
node.retry_cl = node.lastBlockage
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/***********************
|
|
* MAIN EXECUTION CODE *
|
|
***********************/
|
|
|
|
const onLoginPage = window.location.pathname === "/login"
|
|
|
|
// Define some standard header and footer for our pages
|
|
generateRobomergeHeader(!onLoginPage)
|
|
generateRobomergeFooter()
|
|
|
|
// Festive fun! (process after page load with jQuery ready())
|
|
$(function() {
|
|
processHolidays()
|
|
})
|
|
|
|
// Since we are on the login page, do not show login information
|
|
if (!onLoginPage) {
|
|
// This call populates user information and uptime statistics UI elements.
|
|
getBranchList(function(data) {
|
|
let uptime = $('#uptime').empty();
|
|
if (data) {
|
|
clearErrorText();
|
|
handleUserPermissions(data.user);
|
|
|
|
if (data.started){
|
|
uptime.append($('<b>').text("Running since: ")).append(new Date(data.started).toString());
|
|
}
|
|
else {
|
|
document.title = "ROBOMERGE (stopped)" + document.title
|
|
setErrorText('Not running');
|
|
}
|
|
|
|
if (data.user) {
|
|
const $container = $('#signed-in-user').removeClass('hidden');
|
|
$('.user-name', $container).text(data.user.displayName);
|
|
$('.tags', $container).text(data.user.privileges && Array.isArray(data.user.privileges) ? ` (${data.user.privileges.join(', ')})` : '');
|
|
|
|
if (data.insufficientPrivelege) {
|
|
displayErrorMessage('There are bots running but logged in user does not have access to see any');
|
|
}
|
|
|
|
}
|
|
else {
|
|
$('#signed-in-user').addClass('hidden');
|
|
}
|
|
|
|
if (data.version) {
|
|
$('#version').text(data.version);
|
|
}
|
|
}
|
|
});
|
|
}
|