NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
/* eslint-disable no-undef */
/* eslint-disable no-inner-declarations */
// ==UserScript==
// @name CPQ Performance Stats
// @version 0.230
// @description CPQ Config Model Performance Stats
// @author Obada Kadri
// @license MIT
// @match *://*.bigmachines.com/commerce/display_company_profile.jsp*
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js
// @require https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js
// @require https://cdn.jsdelivr.net/chartist.js/latest/chartist.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.8.0/js/bootstrap-datepicker.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.23/moment-timezone-with-data.min.js
// @grant GM_addStyle
// ==/UserScript==
// Defaults and presets
var minUsagePerDay = 3; // The minimum number of referenceObject hits per day to consider on the chart
var queryTypes = ["configure", "update"];
var currentCallId = 0;
var MODELS = [
// HW/SW
{
name: "Hardware : HW/SW Part Search",
id: "allEquipment:hardwareSoftware:pTOSoftwareServices:hWSWPartSearch"
},
{
name: "Hardware : Hardware Services",
id: "allEquipment:hardwareSoftware:professionalServices:hardwareServices"
},
{
name: "Hardware : Micros Part Search (RGBU)",
id: "allEquipment:hardwareSoftware:microsParts:microsPartSearch"
},
{
name: "Hardware : Micros Part Search (HGBU)",
id: "allEquipment:hardwareSoftware:microsParts:microsPartSearch_HWSW"
},
{
name: "Hardware : Hardware Services",
id: "allEquipment:hardwareSoftware:professionalServices:hardwareServices"
},
{
name: "Software : Software Part Search",
id: "allEquipment:software:softwarePartSearch:softwarePartSearchModel"
},
{
name: "Software : Support Only and Subscription Offerings",
id: "allEquipment:software:softwarePartSearch:supportOnly"
},
{
name: "Software : Software Services",
id: "allEquipment:hardwareSoftware:professionalServices:softwareServices"
},
{
name: "License Subscriptions : Tech License Subscriptions",
id: "allEquipment:licenseSubscription:licenseSubscription:techLicenseSubscription"
},
];
// Don't run on login page
var notLoginPage = !document.querySelector("#login-form .login-links");
if (notLoginPage) {
var DAILY_USAGE_CHART = "daily-usage-chart";
var DAILY_SPEED_CHART = "daily-speed-chart";
$("head").append(
"<link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css\">"
);
$("head").append(
"<link rel=\"stylesheet\" href=\"//cdn.jsdelivr.net/chartist.js/latest/chartist.min.css\">"
);
$("head").append(
"<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.8.0/css/bootstrap-datepicker.min.css\">"
);
/**
* Calculates the average of a given number array, handles non numbers
* @param arr
*/
function average(arr) {
var sum = 0;
var count = 0;
arr.forEach(i => {
if (typeof i == "number" && !isNaN(i)) {
sum += i;
count++;
}
});
return {
sum: sum,
count: count,
average: count ? Math.round(sum / count) : 0
};
}
/**
* Returns an object with dates as keys, and each value is the sum, count, and average of that date
*/
function getChartDailyStats(dataItems) {
var dailyStats = {};
var dataKeys = [];
dataItems.forEach(function(item) {
var itemDate = item.eventDate ? moment(item.eventDate).tz("America/Los_Angeles").format("YYYY-MM-DD") : "";
if (itemDate) {
if (!dailyStats[itemDate]) {
dataKeys.push(itemDate);
dailyStats[itemDate] = {
serverTimes: []
};
}
dailyStats[itemDate].serverTimes.push(item.serverTime);
}
});
dataKeys.forEach(function(key) {
if (dailyStats[key].serverTimes.length >= minUsagePerDay) {
// Ignore stats for days where there are low number of visits
dailyStats[key].averageServerTime = average(
dailyStats[key].serverTimes
);
} else {
delete dailyStats[key];
}
});
console.info(dailyStats);
return dailyStats;
}
/**
* Draws the Chartist bar chart for the given data on the div of the given id
* @param {*} chartId
* @param {*} dailyStats
*/
function drawChartist(chartId, dailyStats) {
dailyStats = dailyStats || {};
var labels = [];
var dataSeries = [];
var keys = Object.keys(dailyStats);
keys.sort();
for (var i = 0; i < keys.length; ++i) {
labels.push(keys[i].substr(5));
if (chartId == DAILY_USAGE_CHART) {
dataSeries.push(dailyStats[keys[i]].averageServerTime.count);
} else if (chartId == DAILY_SPEED_CHART) {
dataSeries.push(
(dailyStats[keys[i]].averageServerTime.average / 1000).toFixed(2)
);
}
}
var data = {
labels: labels,
series: [dataSeries]
};
var options = {
height: "300px"
};
// eslint-disable-next-line no-undef
new Chartist.Bar("#" + chartId, data, options);
}
/**
* Calls the CPQ API URL(s) and returns the performance logs of the given referenceObject for the period given
* @param {*} referenceObject
* @param {*} dateFrom
* @param {*} dateTo
* @param {*} url
* @param {*} allData
* @param {*} deferred
*/
function getStatsData(
referenceObject,
callId,
dateFrom,
dateTo,
url,
allData,
deferred
) {
deferred = deferred || new $.Deferred();
if (callId === currentCallId) {
if (!url) {
// It looks like MongoDB 'lte' is not working as expected. So I am adding a day to the dateTo and using 'lt' instead
const dateToPlusOne = moment(dateTo, "YYYY-MM-DD").add(1, "days").toISOString();
const dateFromISO = moment(dateFrom, "YYYY-MM-DD").toISOString();
const queryTypesStr = queryTypes.map(qt => `{"component":{$eq:"${qt}"}}`).join(",");
const byModelUrl = `/rest/v6/performanceLogs?q={$and:[{$or:[${queryTypesStr}]},{"referenceObject":{$like:"${referenceObject}"}},{"eventDate":{$gte:"${dateFromISO}"}},{"eventDate":{$lt:"${dateToPlusOne}"}}]}&orderby=eventDate:desc&totalResults=true&offset=0`;
const byTagUrl = `/rest/v6/performanceLogs?q={$and:[{"component":{$like:"${referenceObject}"}},{"eventDate":{$gte:"${dateFromISO}"}},{"eventDate":{$lt:"${dateToPlusOne}"}}]}&orderby=eventDate:desc&totalResults=true&offset=0`;
const searchBy = $("input[name=searchBy]:checked").val();
url = searchBy == "by-referenceObject" ? byModelUrl : byTagUrl;
}
allData = allData || [];
$.get(url, function(data) {
if (data.hasMore && callId === currentCallId) {
var nextLink = data.links.find(link => link.rel === "next").href;
var totalResults = data.totalResults;
allData = allData.concat(data.items);
if ($("#selected-reference-object-stats .progress-bar").length) {
// Update progress bar if exists
$("#selected-reference-object-stats .progress-bar").css(
"width",
`${(100 * allData.length) / totalResults}%`
);
$("#selected-reference-object-stats .progress-bar").text(
`${allData.length} / ${totalResults} Records`
);
} else {
// Append the progress bar
$("#selected-reference-object-stats").html(`
<div class="progress">
<div class="progress-bar bg-danger" style="width: ${(100 * allData.length) /
totalResults}%">${allData.length} / ${totalResults} Records</div>
</div>
`);
}
getStatsData(
referenceObject,
callId,
dateFrom,
dateTo,
nextLink,
allData,
deferred
);
} else {
deferred.resolve({
data: allData.concat(data.items),
callId: callId
});
}
}).fail(function() {
deferred.reject();
});
} else {
deferred.reject();
}
return deferred.promise();
}
/**
* When the referenceObject is selected from the dropdown menu, this function populates the initial API URL and renders the results
* @param {*} referenceObject
*/
function setSelectedModel(referenceObjectId) {
currentCallId++;
var referenceObject = MODELS.find(function(referenceObject) {
return referenceObject.id == referenceObjectId;
});
if (!referenceObject) {
return {};
}
var dateFrom =
$("#dateFrom").val() || new Date().toISOString().split("T")[0];
var dateTo = $("#dateTo").val() || new Date().toISOString().split("T")[0];
// Set the results header
$("#selected-reference-object-header").text(
`${referenceObject.name} Performance Stats:`
);
// Set the performance stats results
$("#reference-object-charts").addClass("d-none");
$("#selected-reference-object-stats").html("Loading...");
getStatsData(referenceObject.id, currentCallId, dateFrom, dateTo)
.then(function(items) {
if (items.callId === currentCallId) {
console.info(items);
var serverTime = average(items.data.map(i => parseInt(i.serverTime)));
$("#reference-object-charts").removeClass("d-none");
var dailyData = getChartDailyStats(items.data);
drawChartist(DAILY_USAGE_CHART, dailyData);
drawChartist(DAILY_SPEED_CHART, dailyData);
// Set the performance stats results
$("#selected-reference-object-stats").html(`
Average Server Time for the entire period: <strong>${(
serverTime.average / 1000
).toFixed(2)} seconds</strong> (${serverTime.count} records)
`);
}
})
.fail(function(error) {
$("#selected-reference-object-stats").html(`Error: ${error}`);
});
}
/**
* When the tag is set, this function populates the initial API URL and renders the results
* @param {*} tag
*/
function setTag(tag) {
currentCallId++;
if (!tag) {
return {};
}
var dateFrom =
$("#dateFrom").val() || new Date().toISOString().split("T")[0];
var dateTo = $("#dateTo").val() || new Date().toISOString().split("T")[0];
// Set the results header
$("#selected-reference-object-header").text(
`Tag "${tag}" Performance Stats:`
);
// Set the performance stats results
$("#reference-object-charts").addClass("d-none");
$("#selected-reference-object-stats").html("Loading...");
getStatsData(tag, currentCallId, dateFrom, dateTo)
.then(function(items) {
if (items.callId === currentCallId) {
console.info(items);
var serverTime = average(items.data.map(i => parseInt(i.serverTime)));
$("#reference-object-charts").removeClass("d-none");
var dailyData = getChartDailyStats(items.data);
drawChartist(DAILY_USAGE_CHART, dailyData);
drawChartist(DAILY_SPEED_CHART, dailyData);
// Set the performance stats results
$("#selected-reference-object-stats").html(`
Average Server Time for the entire period: <strong>${(
serverTime.average / 1000
).toFixed(2)} seconds</strong> (${serverTime.count} records)
`);
}
})
.fail(function(error) {
$("#selected-reference-object-stats").html(`Error: ${error}`);
});
}
/**
* Appends the main button and modal to the CPQ body
*/
function showStats() {
"use strict";
var referenceObjectOptions = MODELS.map(function(referenceObject) {
return `<option value="${
referenceObject.id
}">${referenceObject.name}</option>`;
}).join("\n");
var modalHtml = `
<!-- Modal -->
<div class="modal fade" id="performanceModal" tabindex="-1" role="dialog" aria-labelledby="performanceModalLabel">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content d-none">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Model Performance Stats</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form>
<div class="container form-group">
<div class="row">
<div class="col-sm" align="center">
<h5>From</h5>
<div id="datepickerFrom"></div>
<input type="hidden" id="dateFrom">
</div>
<div class="col-sm" align="center">
<h5>To</h5>
<div id="datepickerTo"></div>
<input type="hidden" id="dateTo">
</div>
</div>
</div>
<div class="form-group">
<div id="search-by-mode" class="btn-group btn-group-toggle justify-content-end" data-toggle="buttons">
<label class="btn btn-secondary active" data-value="by-referenceObject">
<input type="radio" name="searchBy" value="by-referenceObject" autocomplete="off" checked>By Model
</label>
<label class="btn btn-secondary" data-value="by-tag">
<input type="radio" name="searchBy" value="by-tag" autocomplete="off">By LogTime Tag
</label>
</div>
</div>
<div id="by-common" class="form-group">
<label for="min-num">Minimum Number to consider per day</label>
<input type="text" id="min-num" value="${minUsagePerDay}" class="form-control" placeholder="If you choose 3, then any day with less than 3 entries, will not be displayed" />
</div>
<div id="by-referenceObject" class="search-by form-group">
<div id="query-type">
<label for="query-type">Query Types</label><br />
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="query-type[]" id="query-type-1" value="configure" checked="true">
<label class="form-check-label" for="query-type-1">New Config</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="query-type[]" id="query-type-2" value="update" checked="true">
<label class="form-check-label" for="query-type-2">Update</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" name="query-type[]" id="query-type-3" value="addToTxn">
<label class="form-check-label" for="query-type-3">Add to Quote</label>
</div>
</div>
<div id="model-select">
<label for="reference-object-select">Model</label>
<select id="reference-object-select" class="form-control">
<option value=""></option>
${referenceObjectOptions}
</select>
</div>
</div>
<div id="by-tag" class="form-group search-by d-none">
<label for="tag">Tag</label>
<input type="text" id="tag" class="form-control" placeholder="Exact tag passed in the logtime() function" />
</div>
</form>
<div class="part-data part-data-active" id="config-reference-object-parts-data">
<h3 class="header" id="selected-reference-object-header"></h3>
<div id="selected-reference-object-stats"></div>
<div id="reference-object-charts" class="d-none">
<strong>Daily Usage (Number of referenceObject loads / updates)</strong>
<div id="${DAILY_USAGE_CHART}"></div>
<strong>Daily Average Speed (In seconds)</strong>
<div id="${DAILY_SPEED_CHART}"></div>
<p class="text-muted">Not showing days with referenceObject hits less than <span id="min-num-info">${minUsagePerDay}</span></p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
`;
var okToolBar = document.querySelector("ul.ok-tool-bar");
if (!okToolBar) {
okToolBar = document.createElement("ul");
okToolBar.className = "ok-tool-bar";
$("body").append(okToolBar);
}
var showPerfStatsBtn = document.createElement("li");
var text = document.createTextNode("Show Performance Stats");
showPerfStatsBtn.appendChild(text);
showPerfStatsBtn.id = "btn-show-stats";
showPerfStatsBtn.className = "show-stats";
showPerfStatsBtn.dataset.toggle = "modal";
showPerfStatsBtn.dataset.target = "#performanceModal";
//--- Add nodes to page
okToolBar.append(showPerfStatsBtn);
$("body").append(modalHtml);
$("#btn-show-stats").on("click", function() {
$(".modal-content").removeClass("d-none");
});
$("#search-by-mode *").on("click change", function() {
var value = $(this).data("value");
$(".search-by").addClass("d-none");
$("#" + value).removeClass("d-none");
});
$("#reference-object-select").change(function() {
if (
$("#dateFrom").val() &&
$("#dateTo").val() &&
$("#reference-object-select").val()
) {
setSelectedModel($("#reference-object-select").val());
}
});
$("input[name='query-type[]']").change(function(e) {
queryTypes = $.map($("input[name='query-type[]']:checked"), e => $(e).val());
// Handle mutual exclusivity
if (e.currentTarget.value === "addToTxn") {
$('#query-type-1').attr('checked', false);
$('#query-type-2').attr('checked', false);
queryTypes = ["addToTxn"];
} else {
$('#query-type-3').attr('checked', false);
const atqIndex = queryTypes.indexOf("addToTxn");
if (atqIndex >= 0) {
queryTypes.splice(atqIndex, 1);
}
}
if (queryTypes.length === 1) {
$("input[name='query-type[]']:checked").attr("disabled", true);
$("input[name='query-type[]']:not(:checked)").attr("disabled", false);
} else {
$("input[name='query-type[]']").removeAttr("disabled");
}
if (
$("#dateFrom").val() &&
$("#dateTo").val() &&
$("#reference-object-select").val()
) {
setSelectedModel($("#reference-object-select").val());
}
});
$("#tag").change(function() {
var wto;
clearTimeout(wto);
wto = setTimeout(function() {
if ($("#dateFrom").val() && $("#dateTo").val() && $("#tag").val()) {
setTag($("#tag").val());
}
}, 500);
});
$("#min-num").change(function() {
minUsagePerDay = parseInt($("#min-num").val());
$("#min-num-info").text(minUsagePerDay);
var wto;
clearTimeout(wto);
wto = setTimeout(function() {
var searchBy = $("input[name=searchBy]:checked").val();
if ($("#dateFrom").val() && $("#dateTo").val()) {
if (
searchBy == "by-referenceObject" &&
$("#reference-object-select").val()
) {
setSelectedModel($("#reference-object-select").val());
} else if (searchBy == "by-tag" && $("#tag").val()) {
setTag($("#tag").val());
}
}
}, 500);
});
// Date inputs config
$("#datepickerFrom").datepicker({
format: "yyyy-mm-dd",
endDate: "0d"
});
$("#datepickerFrom").on("changeDate", function() {
var dateFrom = $("#datepickerFrom").datepicker("getFormattedDate");
$("#dateFrom").val(dateFrom);
var searchBy = $("input[name=searchBy]:checked").val();
if ($("#dateFrom").val() && $("#dateTo").val()) {
if (
searchBy == "by-referenceObject" &&
$("#reference-object-select").val()
) {
setSelectedModel($("#reference-object-select").val());
} else if (searchBy == "by-tag" && $("#tag").val()) {
setTag($("#tag").val());
}
}
});
$("#datepickerTo").datepicker({
format: "yyyy-mm-dd",
endDate: "0d"
});
$("#datepickerTo").on("changeDate", function() {
var dateTo = $("#datepickerTo").datepicker("getFormattedDate");
$("#dateTo").val(dateTo);
var searchBy = $("input[name=searchBy]:checked").val();
if ($("#dateFrom").val() && $("#dateTo").val()) {
if (
searchBy == "by-referenceObject" &&
$("#reference-object-select").val()
) {
setSelectedModel($("#reference-object-select").val());
} else if (searchBy == "by-tag" && $("#tag").val()) {
setTag($("#tag").val());
}
}
});
}
showStats();
// Styling Stuff
var bm_css_src = `
ul.ok-tool-bar {
position: fixed;
bottom: 0;
right: 10%;
margin: 0;
}
ul.ok-tool-bar li {
display: inline-block;
padding: 3px 6px;
margin: 0 3px;
font-size: 11px;
text-align: center;
white-space: nowrap;
vertical-align: middle;
-ms-touch-action: manipulation;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-image: none;
border: 1px solid transparent;
border-radius: 5px 5px 0 0;
}
li.show-stats {
color: #fff;
background-color: #2647a0;
border-color: #2647a0;
}
h3.header {
font-size: 1.5em;
margin: .5em 0 1em 0;
}
.ct-label.ct-horizontal {
transform: rotate(-90deg);
padding: 5px 0;
white-space: nowrap;
width: 1em !important;
margin: 5px auto;
}
.modal-xl {
width: 80%;
max-width:1200px;
}
body {
margin: auto;
}
#performanceModal .modal-xl {
margin: 0;
width: 100%;
max-width:100%;
}
`;
// eslint-disable-next-line no-undef
GM_addStyle(bm_css_src);
}