Raw Source
smallbonelu / OCV review case tool

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