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); }