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