NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name OCV review case tool
// @namespace http://tampermonkey.net/
// @version 1.1
// @description a tool for reviewing case
// @author Bruce Lu
// @license MIT
// @include https://ocv.microsoft.com/*
// @require https://code.jquery.com/jquery-3.1.1.min.js
// @require https://raw.githubusercontent.com/uzairfarooq/arrive/master/minified/arrive.min.js
// @downloadURL https://openuserjs.org/src/scripts/smallbonelu/OCV_review_case_tool.js
// @updateURL https://openuserjs.org/meta/smallbonelu/OCV_review_case_tool.meta.js
// @grant GM_xmlhttpRequest
// ==/UserScript==
/*
update notes:
1. fixed the review timestamp tool display randomly bug
2. display machine confidence after the issue bucket
3. display personal daily reviewed count every 2 min
4. avoid reviewing one case repeatly by pressing shortcut key 'r' multiple times
*/
let reviewNotesSectionSelector =
"body > div:nth-child(49) > div > div.view-port > div > div.triage.container-fluid > div > div.item-detail-pane.col-xs-8 > div > item-details > div > div > div > div:nth-child(3) > div.clean-tabs > span:nth-child(5) > a";
let reviewNotesSaveBtnSelector =
"body > div:nth-child(49) > div > div.view-port > div > div.triage.container-fluid > div > div.item-detail-pane.col-xs-8 > div > item-details > div > div > div > div:nth-child(3) > div.tab-content > div > div.review-notes-section > button";
let myFeedbackLinkSelector = "a[log-event='View My Feedback']";
let notesTextAreaSelector =
"body > div:nth-child(49) > div > div.view-port > div > div.triage.container-fluid > div > div.item-detail-pane.col-xs-8 > div > item-details > div > div > div > div:nth-child(3) > div.tab-content > div > div:nth-child(1) > textarea";
let issuesListSelector =
".view-port > div > div.triage.container-fluid > div > div.item-detail-pane.col-xs-8 > div > item-details > div > div > div > div.item-details-key-details > div.fields-column > table > tbody > tr:nth-child(2) > td.field-value > tag-list-with-states > div > span";
let ribbonSelector = ".view-port .ribbon";
let MIN = 4;
let MAX = 5;
let isShortcut = false;
let alias;
let issuesStatusList;
let reviewNotesSection;
let isMenuAdded = false;
const baseURL = "https://ocv.microsoft.com/api/";
const tokenKey =
"adal.access.token.keyhttps://microsoft.onmicrosoft.com/ocvwebapi";
function getToken(key) {
return localStorage.getItem(key);
}
function fetchOCVData(payloadString, apiString) {
return $.ajax({
method: "POST",
url: baseURL + apiString,
contentType: "application/json;charset=UTF-8",
headers: {
Authorization: "Bearer " + getToken(tokenKey),
},
data: payloadString,
}).done(function (data) {
console.log(apiString, data);
return data;
});
}
function parseNestedQueryString(queryString) {
let apiString = "ParseNestedQueryString";
return fetchOCVData(queryString, apiString);
}
async function getDailyReviewedCount() {
let seachApi = "es/ocv/_search";
let dailyQueryString = `OcvAreas:(SetDate:${getCurrentDate(
"-"
)} AND (SetBy:"${alias}"))`;
let parsedQuery = await parseNestedQueryString(dailyQueryString);
let payloadString = {
_source: {
excludes: [
"MlTextProcessingOutput",
"SysSieveTags",
"AlchemyIssuesHidden",
"UnclassifiableTaxonomies",
"ABConfigs",
"AFDFlightInfo",
"Flights",
"ProcessSessionTelemetry",
"WatsonCrashData",
"Telemetry",
"ResponseHistory",
"History",
"AutomatedEmailState",
"OcvAreasHidden",
"OcvIssuesHidden",
],
},
size: 0,
query: {
bool: {
filter: {
bool: {
must: [
{
range: {
CreatedDate: {
gte: "2019-01-19T16:00:00.000Z",
lte: new Date().toISOString(),
},
},
},
{
bool: {
filter: {
nested: parsedQuery.query.nested,
},
},
},
],
},
},
},
},
};
let dailyReviewedData = await fetchOCVData(
JSON.stringify(payloadString),
seachApi
);
console.log(`Your daily reviewed cases is: ${dailyReviewedData.hits.total}`);
updateNodeText("#daily-reviewed-count", dailyReviewedData.hits.total);
}
function presentPredictionConfidence(node, confidence) {
let span = document.createElement("span");
let percent = confidence.match(/\d+(\.*\d*\%|\s\bpercent\b)/);
if (node.querySelector("#percent")) return false;
span.setAttribute("id", "percent");
span.innerText = (percent && percent[0]) || "";
node.appendChild(span);
return true;
}
function getCurrentDate(splitor) {
splitor = splitor || "/";
let date = new Date();
let year = date.getFullYear();
let month = date.getMonth() + 1;
let day = date.getDate();
return `${year}${splitor}${month < 10 ? "0" + month : month}${splitor}${
day < 10 ? "0" + day : day
}`;
}
function getAlias() {
return new Promise((resolve, reject) => {
$(".ocv").arrive(myFeedbackLinkSelector, function () {
let myFeedbackLink = $(this)[0];
let str = decodeURI(myFeedbackLink.href);
alias = str.match(/([\w-]+)@[a-zA-Z_]+?\.[a-zA-Z]{2,3}/)[1];
console.log("Get alias: ", alias);
if (alias.length > 3) {
resolve(alias);
} else {
reject("cannot get the alias");
}
});
});
}
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min; //inlcude maximum and minimum value
}
function getTriagingIssuesStatus() {
let issuesListLink = document.querySelectorAll(issuesListSelector);
let reviewIssue = getTriagingSuggestion();
let issueStatus = {};
if (issuesListLink.length === 0) return issueStatus;
issuesListLink.forEach((issue) => {
let item = issue.querySelector("a.area-toggle");
let issueText = item.text.trim() || "";
let status = item.title.trim() || "";
if (issueText === reviewIssue) {
issueStatus.issue = issueText;
issueStatus.status = status;
presentPredictionConfidence(issue, status);
}
});
return issueStatus;
}
function isTriagingIssuesReviewed() {
let issueStatus = getTriagingIssuesStatus();
if (issueStatus.status.indexOf("Not Reviewed") === -1) {
return true;
}
return false;
}
function getTriagingSuggestion() {
try {
let triagingSuggestionsList = document
.querySelector("div.header-bar > span")
.textContent.split(":");
if (triagingSuggestionsList.length > 1) {
return triagingSuggestionsList[triagingSuggestionsList.length - 1].trim();
}
} catch (error) {
console.error("failed to get the triaging suggestion text");
return "";
}
}
function addNotes(node) {
return new Promise((resolve, reject) => {
let date = getCurrentDate();
let reivewNotesTime = getRandomIntInclusive(MIN, MAX);
// Get the elements
let issuesText = getTriagingSuggestion();
// Fill the reivew notes
if (node.value === "") {
console.log("Add review notes...");
let reviewNotes = `${date}, ${alias}, ${reivewNotesTime}:\n${alias}, ${issuesText}`;
node.value = reviewNotes;
node.dispatchEvent(new Event("change"));
resolve();
} else {
reject();
}
});
}
function changeReviewNotesTime(e) {
e.stopPropagation();
e.preventDefault();
let min = document.getElementById("min-time").value || MIN;
let max = document.getElementById("max-time").value || MAX;
if (parseInt(min) > parseInt(max)) {
alert(`Notes time invalid, max value must larger than min value. `);
return false;
} else {
MIN = min;
MAX = max;
alert(`Notes time has changed between ${MIN} - ${MAX} `);
return true;
}
}
function updateNodeText(selector, count) {
let element = document.querySelector(selector);
if (element) {
element.innerText = count;
}
}
function matchedURL(keyword) {
let hash = location.hash;
return hash.startsWith(keyword) ? true : false;
}
function insertMenu(targetSelector) {
console.log("Add util tool to the page");
getDailyReviewedCount();
let targetEle = document.querySelector(targetSelector);
let myDailReviewedURL = `https://ocv.microsoft.com/#/discover/?searchtype=OcvItems&relDateType=all&offset=0&q=OcvAreas:(SetDate:${getCurrentDate(
"-"
)} AND (SetBy:"${alias}"))&allAreas`;
let ultilHTML = `
<div id='util-container' style="position: fixed; width: 250px; top: 90px; right: 20px; overflow: hidden; z-index: 9999">
<div>
<a href=${encodeURI(
myDailReviewedURL
)} target='_blank' onclick="event.stopPropagation();"
style="background-color: #005A9E; color: white; display: inline-block;">DailyReviewed: </a>
<span id="daily-reviewed-count"></span>
</div>
<div id="notes-time-controller" style="margin-top: 10px;">
<p style="margin: 0;">Notes Time</p>
<form>
<input type="number" name="min" class="form-control" id="min-time" onclick="event.stopPropagation();" placeholder="min" value=${MIN} style="display: inline-block; width: 30%;" min="1" max="8">
<input type="number" name="max" class="form-control" id="max-time" onclick="event.stopPropagation();" placeholder="max" value=${MAX} style="display: inline-block; width: 30%;" min="1" max="8">
<button type="button" class="btn btn-primary" id="apply-btn" style="margin-top: -5px;">Apply</button>
</form>
</div>
</div>
`;
targetEle.insertAdjacentHTML("beforeend", ultilHTML);
isMenuAdded = true;
let applyBtn = document.getElementById("apply-btn");
applyBtn.addEventListener("click", changeReviewNotesTime);
return isMenuAdded;
}
function bulkReviewCase(e) {
if (e.keyCode === 82) {
isShortcut = true;
//if triagging issues has been reviewed, function returned to avoid repeat reiview action.
if (isTriagingIssuesReviewed()) {
console.warn("the issue has been reviewed");
e.preventDefault();
return false;
}
reviewNotesSection.click();
console.log("Saving the review notes...");
}
}
function reviewNotesChangeHandler() {
// Waiting for reivew notes section element loaded
$(".ocv").arrive(reviewNotesSectionSelector, function () {
update();
});
}
function update() {
console.log("get update review notes");
isShortcut = false;
reviewNotesSection = document.querySelector(reviewNotesSectionSelector);
console.log("reviewNotesSection", reviewNotesSection);
getTriagingIssuesStatus();
if (!isMenuAdded) insertMenu(ribbonSelector);
reviewNotesSection.onclick = function () {
// add watcher for the reviewNotesSaveBtn element getCreatedElement
$(".view-port").arrive(reviewNotesSaveBtnSelector, function () {
let notesTextArea = document.querySelector(notesTextAreaSelector);
if (notesTextArea.value !== "") {
return false;
}
let reviewNotesSaveBtn = $(this)[0];
addNotes(notesTextArea).then(() => {
if (isShortcut) {
reviewNotesSaveBtn && reviewNotesSaveBtn.click();
return true;
}
});
});
};
// When press review shortcut key 'r', click the reivew notes section
document.onkeydown = bulkReviewCase;
}
async function init() {
console.log("init the tools...");
let timer;
if (timer) clearInterval(timer);
try {
reviewNotesChangeHandler();
await getAlias();
timer = setInterval(getDailyReviewedCount, 2 * 60 * 1000);
} catch (error) {
console.error("failed to init the tool\n", error);
}
}
init();