blob: fae3ed8368f5e8865ef34be50bed00225e565662 [file]
'use strict';
/**
* This angular.js module pulls together the UI and logic.js.
* It only contains code to make requests to the backend and to implement
* UI logic. All application logic is in logic.js.
*/
// Add everything to the skia namespace.
var skia = skia || {};
(function (ns) {
var app = angular.module('rbtApp', ['ngRoute']);
// Configure the different within app views.
app.config(['$routeProvider', function($routeProvider) {
$routeProvider.when(ns.c.URL_COUNTS + '/:id?', {
templateUrl: 'partials/counts-view.html',
controller: 'CountsCtrl',
reloadOnSearch: false
});
$routeProvider.when(ns.c.URL_TRIAGE, {
templateUrl: 'partials/triage-view.html',
controller: 'TriageCtrl',
reloadOnSearch: false
});
$routeProvider.when(ns.c.URL_TRIAGE + '/:id', {
templateUrl: 'partials/triage-details-view.html',
controller: 'TriageDetailsCtrl',
reloadOnSearch: false
});
$routeProvider.otherwise({redirectTo: ns.c.URL_TRIAGE });
}]);
/*
* CountsCtrl controlls the UI on the main view where an overview of
* test results is presented.
*/
app.controller('CountsCtrl', ['$scope', '$routeParams', '$location', '$timeout', 'dataService',
function($scope, $routeParams, $location, $timeout, dataService) {
// Get the path and use it for the backend request
var testName = ($routeParams.id && ($routeParams.id !== '')) ?
$routeParams.id : null;
// Load counts for all tests of a tile.
$scope.loadCounts = function () {
$scope.state = 'loading';
dataService.loadData(ns.c.URL_COUNTS, $scope.query).then(
function (serverData) {
if (!serverData) {
retry();
return serverData;
};
var temp = ns.processCounts(serverData, testName);
// plug into the sk-plot directive
$scope.plotData = temp.plotData;
$scope.plotTicks = temp.getTicks.bind(temp);
// used to render information about tests
$scope.allAggregates = temp.allAggregates;
$scope.allTests = temp.testDetails;
$scope.allParams = ns.getSortedParams(serverData, false);
updateQueryStr(serverData.query || {});
$scope.state = 'ready';
},
function (errResp) {
retry();
console.log("Error:", errResp);
});
};
function retry() {
$scope.state = 'retry';
$timeout($scope.loadCounts, $scope.reloadInterval * 1000);
}
function updateQueryStr(newQuery) {
$scope.query = newQuery;
$location.search(newQuery);
$scope.qStr = ns.extractQueryString($location.url());
}
// initialize the members and load the data.
$scope.reloadInterval = 3;
$scope.allTests = [];
$scope.plotData = [];
$scope.plotTicks = null;
$scope.oneTest = !!testName;
$scope.allParams = [];
updateQueryStr($location.search());
$scope.loadCounts();
}]);
/*
* TriageCtrl is the controller for the macro triage view. It manages the UI
* and backend requests. Processing is delegated to the functions in the
* 'skia' namespace (implemented in logic.js).
*/
app.controller('TriageCtrl', ['$scope', '$routeParams', '$location', '$timeout', 'dataService',
function($scope, $routeParams, $location, $timeout, dataService) {
var triageStateManager;
var completeTestSet, untriagedTestsOnly;
// var flagsQuery = {};
var blameLists = {};
var allCommits = [];
var sortFn = function(a,b) {
return (a.name === b.name) ? 0 : (a.name < b.name) ? -1 : 1;
};
function processServerData(promise) {
promise.then(
function (serverData) {
if (!serverData) {
retry();
return serverData;
};
// Get all the triage data and sort the tests.
var temp = ns.extractTriageData(serverData, true);
temp.tests.sort(sortFn);
triageStateManager.setNewState(temp.triageState);
// Build the blamelist for quick lookup.
blameLists = {};
if (serverData.blames) {
for(var k in serverData.blames) {
if (serverData.blames.hasOwnProperty(k)) {
var blameObj = {};
for(var i=0, len=serverData.blames[k].length; i<len; i++) {
blameObj[serverData.blames[k][i].digest] = serverData.blames[k][i];
}
blameLists[k] = blameObj;
}
}
}
allCommits = serverData.commits;
completeTestSet = temp.tests;
untriagedTestsOnly = temp.tests.filter(
function(t) { return t.untStats.unique > 0; });
$scope.updateContent();
$scope.allParams = temp.allParams;
$scope.crLinks =temp.commitRanges;
setQuery(serverData.query || {});
$scope.displayQuery = serverData.query || {};
$scope.selectionInfo = {
untStats: temp.untStats,
posStats: temp.posStats,
negStats: temp.negStats
}
$scope.state = 'ready';
$scope.checkStatus();
},
function (errResp) {
retry();
console.log("Error:", errResp);
$scope.checkStatus();
});
}
$scope.getBlames = function (oneTest) {
var ret = [];
var uniques = {};
var digests = (oneTest.collapsed) ?
oneTest.untriaged.map(function(x) { return x.digest; }) :
[oneTest.untriaged.digest];
for(var dIdx=0, dLen=digests.length; dIdx < dLen; dIdx++) {
var arr = blameLists[oneTest.name][digests[dIdx]].freq;
var commitsRange = allCommits.slice(-arr.length)
var maxVal = Math.max.apply(null, arr)
for(var i=0, len=arr.length; i<len; i++) {
if ((arr[i] == maxVal) && (!uniques[commitsRange[i].author])) {
ret.push(commitsRange[i].author);
uniques[commitsRange[i].author]=true;
}
}
}
return ret;
};
// Load triage data across tests.
$scope.loadAllTriageData = function () {
$scope.state = 'loading';
processServerData(dataService.loadData(ns.c.URL_TRIAGE, getCombinedQuery()), true);
};
$scope.saveTriageState = function() {
$scope.state = 'saving';
triageStateManager.saveTriageState(function (promise) {
promise.then(
function (resp) {
$scope.loadAllTriageData();
},
function (errResp) {
console.log(errResp);
});
});
};
function retry() {
$scope.state = 'retry';
$timeout($scope.loadCounts, $scope.reloadInterval * 1000);
}
$scope.selectCommitRange = function(commitRange) {
if (commitRange) {
$scope.commitRangeQuery[ns.c.QUERY_COMMIT_START] = commitRange.start.hash;
$scope.commitRangeQuery[ns.c.QUERY_COMMIT_END] = '';
}
$scope.loadAllTriageData();
};
$scope.resetCommitRangeQuery = function () {
$scope.commitRangeQuery = angular.copy($scope.originalCommitRange);
};
function setQuery(newQuery) {
var q = ns.splitQuery(newQuery, $scope.allParams);
$scope.filteredQuery = q.paramQuery;
$scope.commitRangeQuery = q.commitRangeQuery;
$scope.originalCommitRange = angular.copy(q.commitRangeQuery);
$scope.flagsQuery = q.flagsQuery;
$scope.headSelected = ns.flatQueryValueOr($scope.flagsQuery, ns.c.QUERY_HEAD, '0');
$location.search(newQuery);
$scope.qStr = ns.extractQueryString($location.url());
}
function getCombinedQuery() {
var result = ns.unionObject($scope.filteredQuery,
$scope.commitRangeQuery, $scope.flagsQuery);
return result;
}
$scope.$watch('commitRangeQuery', function() {
$scope.crClean = angular.equals($scope.commitRangeQuery, $scope.originalCommitRange);
}, true);
// Pick the subset of content we are interested in.
$scope.updateContent = function () {
var t = ($scope.showUntriagedOnly) ? untriagedTestsOnly : completeTestSet;
$scope.paginator.setContent(t, $scope.collapseTests);
};
$scope.getImageLists = function(oneTest) {
if (oneTest.untStats.unique > 0) {
pos = oneTest.untriaged.diffs;
}
return {
positive: pos,
negative: neg,
untriaged: unt
};
}
// unroll converts the tests with nested untriaged digests into a flat
// list.
function unroll(tests, collapse) {
var result = [];
var rs, pos, diff;
if (collapse) {
for(var i=0, len=tests.length; i<len; i++) {
result.push({
name: tests[i].name,
showName: true,
collapsed: true,
posUnique: tests[i].posStats.unique,
negUnique: tests[i].negStats.unique,
untUnique: tests[i].untStats.unique,
pos: null,
untriaged: tests[i].untriaged,
diff: null,
});
}
} else {
for(var i=0, len=tests.length; i<len; i++) {
rs = (tests[i].untStats.unique > 0) ? tests[i].untStats.unique : 1;
for(var j=0; j < rs; j++) {
pos = (tests[i].untriaged[j] && tests[i].untriaged[j].diffs[0]) ? tests[i].positiveMap[tests[i].untriaged[j].diffs[0].posDigest] : false;
diff = (tests[i].untriaged[j]) ? tests[i].untriaged[j].diffs[0] : false;
result.push({
name: tests[i].name,
showName: j === 0,
collapsed: false,
posUnique: tests[i].posStats.unique,
negUnique: tests[i].negStats.unique,
untUnique: tests[i].untStats.unique,
pos: pos,
untriaged: tests[i].untriaged[j],
diff: diff
});
}
}
}
return result;
};
$scope.headChanged = function() {
$scope.flagsQuery[ns.c.QUERY_HEAD] = [$scope.headSelected];
$scope.loadAllTriageData();
};
// initialize the members and load the data.
function init() {
// Initialize the triage state manager.
triageStateManager = new TriageStateManager($scope, dataService);
$scope.reloadInterval = 3;
$scope.allTests = [];
$scope.allParams = [];
$scope.crLinks = [];
$scope.imageSize = 100;
$scope.showUntriagedOnly = true;
$scope.collapseTests = true;
$scope.pageSize = 50;
$scope.paginator = new Paginator($scope.pageSize, 5, paginateTests, function(page) {
$scope.allTests = unroll(page, $scope.collapseTests);
});
// Inject the constants into the scope.
$scope.c = ns.c;
setQuery($location.search());
$scope.loadAllTriageData();
};
init();
// Register for corpus changes.
$scope.$on('corpus-change', function() {
init();
});
}]);
/**
* Class TriageStateManager wraps around a scope and handles changes in
* the triage state.
*/
function TriageStateManager($scope, dataService) {
// Keep a reference to the scope and initialize it.
this.$scope = $scope;
this.dataService = dataService;
this.$scope.initialTriageState = {};
this.$scope.triageState = {};
this.$scope.triageStateDelta = {};
this.updatedTriageState(null);
// Inject the functions into the scope. Make sure to bind the object.
this.$scope.setTriageState = this.setTriageState.bind(this);
this.$scope.resetTriageState = this.resetTriageState.bind(this);
}
TriageStateManager.prototype.setNewState = function(triageState) {
this.$scope.initialTriageState = triageState;
this.$scope.triageState = angular.copy(triageState);
this.updatedTriageState();
};
/** updatedTriageState checks whether the currently assigned labels
* have changed. This is used to enable/disable the save and reset
* buttons among other things.
*/
TriageStateManager.prototype.updatedTriageState = function(testName) {
this.$scope.triageStateDirty =
!angular.equals(this.$scope.triageState, this.$scope.initialTriageState);
this.$scope.pendingCount = ns.updateDelta(this.$scope.triageState,
this.$scope.initialTriageState, this.$scope.triageStateDelta, testName);
};
// setTriageState sets the label of the given untriaged digest.
// If changeData is an array it will set the labels for all digests in the array.
TriageStateManager.prototype.setTriageState= function (testName, changeData, value) {
if (changeData.constructor === Array) {
for(var i=0, len=changeData.length; i < len; i++) {
this.$scope.triageState[testName][changeData[i].digest] = value;
}
} else if (typeof(changeData) === 'object') {
for(var testName in changeData) {
if (changeData.hasOwnProperty(testName)) {
for (var digest in changeData[testName]) {
if (changeData[testName].hasOwnProperty(digest)) {
this.$scope.triageState[testName][digest] = value;
}
}
}
}
testName = null;
} else {
this.$scope.triageState[testName][changeData] = value;
}
this.updatedTriageState(testName);
};
// resetTriageState clears all labels of the untriaged digests.
TriageStateManager.prototype.resetTriageState = function () {
this.$scope.triageState = angular.copy(this.$scope.initialTriageState);
this.updatedTriageState();
};
// saveTriageState saves the labeled untriaged digest to the backend
// and retrieves the new triage state for this test.
TriageStateManager.prototype.saveTriageState = function (responseFn) {
var req = new ns.TriageDigestReq();
for(var testName in this.$scope.triageStateDelta) {
if (this.$scope.triageStateDelta.hasOwnProperty(testName)) {
var delta = this.$scope.triageStateDelta[testName].delta;
for(var digest in delta) {
if (delta.hasOwnProperty(digest)) {
req.add(testName, digest, this.$scope.triageState[testName][digest]);
}
}
}
}
responseFn(this.dataService.sendData(ns.c.URL_TRIAGE, req));
};
// paginateTests splits the array of tests into pages with the given
// pageSize. Each untriaged digest in a test is considered an entry that
// counts towards the pagesize. Each page will contain at least one complete
// test. The number of images might be larger or smaller than page size as
// a result.
function paginateTests(pageSize, tests, collapse) {
var pages = [];
var current =[];
var currentCounter = 0, incVal;
if (collapse) {
for(var i=0, len=tests.length; i < len; i += pageSize) {
pages.push(tests.slice(i, i+pageSize));
}
} else {
for(var i=0, len=tests.length; i < len;) {
incVal = tests[i].untStats.unique > 0 ? tests[i].untStats.unique : 1;
current.push(tests[i]);
currentCounter += incVal;
i++;
while(i < len) {
incVal = tests[i].untStats.unique > 0 ? tests[i].untStats.unique : 1;
if ((currentCounter + incVal) > pageSize) {
break;
}
current.push(tests[i]);
currentCounter += incVal;
i++;
}
pages.push(current);
current = [];
currentCounter = 0;
}
}
return pages;
};
// Paginator is a helper class that manages pagination state.
function Paginator(pageSize, rangeLen, paginationFn, callback) {
this.pageSize = pageSize;
this.rangeLen = rangeLen;
this.callback = callback;
this.paginationFn = paginationFn;
this.len = 0;
this.rangeOffset = Math.floor(this.rangeLen/2);
}
Paginator.prototype.setContent = function( _args_ ) {
var args = [this.pageSize];
for(var i =0; i < arguments.length; i++) {
args.push(arguments[i]);
}
this.pages = this.paginationFn.apply(null, args);
this.len =this.pages.length;
this.lastIndex = this.pages.length-1;
this.current = -1;
this.gotoPage(0);
};
Paginator.prototype.gotoPage = function(targetPage) {
if ((this.len == 0) || ((targetPage >= 0) && (targetPage <= this.lastIndex))) {
this.current = targetPage;
this.first = this.current === 0;
this.last = this.current === this.lastIndex;
this.recalcRange();
this.currentPage = this.pages[this.current] || [];
this.callback(this.currentPage);
};
};
Paginator.prototype.recalcRange = function() {
var start = Math.max(Math.min(this.current - this.rangeOffset, this.len-this.rangeLen), 0);
var end = Math.min(start + this.rangeLen - 1, this.lastIndex);
this.range = [];
for(var i=start; i <= end; i++) {
this.range.push(i);
}
this.rangeStart = this.range[0];
};
Paginator.prototype.gotoPrevious = function() {
if (this.current > 0) {
this.gotoPage(this.current-1);
}
};
Paginator.prototype.gotoNext = function() {
if (this.current < this.lastIndex) {
this.gotoPage(this.current+1);
}
};
Paginator.prototype.gotoFirst = function() {
this.gotoPage(0);
};
Paginator.prototype.gotoLast = function() {
this.gotoPage(this.lastIndex);
};
/*
* TriageDetailsCtrl is the controller for the micro triage view. It manages the UI
* and backend requests. Processing is delegated to the functions in the
* 'skia' namespace (implemented in logic.js).
*/
app.controller('TriageDetailsCtrl', ['$scope', '$routeParams', '$location', '$timeout', 'dataService',
function($scope, $routeParams, $location, $timeout, dataService) {
// Get the path and use it for the backend request
var testName = $routeParams.id;
var path = $location.path();
var positives, negatives;
var posIndex, negIndex;
var triageStateManager;
var commitsMap, allCommits;
var blameLists;
// processServerData is called by loadTriageState and also saveTriageState
// to process the triage data returned by the server.
function processServerData(promise, updateQuery) {
promise.then(
function (serverData) {
if (!serverData) {
retry();
return serverData;
};
var triageData = ns.extractTriageData(serverData, true);
var testData = triageData.tests[0];
$scope.untStats = testData.untStats;
$scope.posStats = testData.posStats;
$scope.negStats = testData.negStats;
// TODO(stephana): Factor commits handling into a separate REST endpoint.
commitsMap = serverData.commitsByDigest && serverData.commitsByDigest[testName] || {};
allCommits = serverData.commits;
blameLists = {};
for(var i=0, len=serverData.blames[testName].length; i<len; i++) {
blameLists[serverData.blames[testName][i].digest] = serverData.blames[testName][i].freq;
}
$scope.untriaged = testData.untriaged;
triageStateManager.setNewState(triageData.triageState);
$scope.untIndex = -1;
$scope.leftIndex = -1;
posIndex = (testData.positive.length > 0) ? 0 : -1;
negIndex = (testData.negative.length > 0) ? 0 : -1;
$scope.showPositives = true;
$scope.selectUntriaged(0);
// If there are no untriaged we need to just set the positive
// values since they are no longer a function of the untriaged.
if ($scope.untriaged.length === 0) {
positives = testData.positive;
};
negatives = testData.negative;
// Force the left column to positives since they are initialized.
$scope.switchLeftColumn(true);
$scope.allParams = triageData.allParams;
// Show the source images if there are no positives.
$scope.showSrcImages = $scope.currentUntriaged &&
!$scope.currentLeft;
if (updateQuery) {
$scope.query = serverData.query || {};
$location.search($scope.query);
}
$scope.state='ready';
},
function (errResp) {
retry();
console.log("Error:", errResp);
});
}
$scope.saveTriageState = function() {
$scope.state = 'saving';
triageStateManager.saveTriageState(function (promise) {
promise.then(
function(posResponse) {
$scope.loadTriageData();
},
function (errResp) {
console.log("Error. Save failed:", errResp);
});
});
};
// loadTriageData sends a GET request to the backend to get the
// untriaged digests.
$scope.loadTriageData = function () {
$scope.state = 'loading';
processServerData(dataService.loadData(path, $scope.query), true);
};
function retry() {
$scope.state = 'retry';
$timeout($scope.loadTriageData, $scope.reloadInterval * 1000);
}
function getCommitsList(digest, all) {
if (!blameLists || !blameLists[digest]) {
return null;
}
var commits = [];
var blameCommits = allCommits.slice(allCommits.length-blameLists[digest].length);
for(var i=0, len=blameLists[digest].length; i < len; i++) {
if (!all && blameLists[digest][i]===0) {
break;
}
commits.push({
hash: blameCommits[i].hash,
count: blameLists[digest][i],
commit_time: blameCommits[i].commit_time,
author: blameCommits[i].author
});
}
return {
commits: commits,
hasMore: commits.length !== blameLists[digest].length
};
}
$scope.expandCommitsList = function () {
if ($scope.commitsList && $scope.commitsList.hasMore) {
$scope.commitsList = getCommitsList($scope.currentUntriaged.digest, true);
}
};
// selectUntriaged is a UI function to pick an untriaged digest.
$scope.selectUntriaged = function (idx) {
if ($scope.untriaged.length === 0) {
$scope.untIndex = -1;
$scope.currentUntriaged = null;
}
else {
$scope.currentUntriaged = $scope.untriaged[idx];
positives =
ns.getSortedPositivesFromUntriaged($scope.currentUntriaged);
$scope.untIndex = idx;
$scope.commitsList = getCommitsList($scope.currentUntriaged.digest, false);
setPosIndex(0);
}
};
// selectLeft is a UI function to pick a positive digest.
$scope.selectLeft = function (idx) {
if ($scope.leftDigests.length === 0) {
$scope.currentLeft = null;
$scope.leftIndex = -1;
} else {
$scope.currentLeft = $scope.leftDigests[idx];
$scope.leftIndex = idx;
}
};
// setPosIndex changes the currently selected positive image whether
// it is displayed or not.
function setPosIndex(idx) {
posIndex = idx;
if ($scope.showPositives) {
$scope.leftDigests = positives;
$scope.selectLeft(posIndex);
}
};
// filterByParams adds the given parameter to the current query and
// triggers a refresh with the newly set filter.
$scope.filterByParam = function(param, value, add) {
if (add) {
$scope.query[param] = $scope.query[param] || [];
if ($scope.query[param].indexOf(value) === -1) {
$scope.query[param].push(value);
}
} else {
$scope.query = {};
$scope.query[param] = value;
}
$scope.loadTriageData();
};
// switchLeftColumn changes which set of digests (positive/negative) is
// displayed in the left hand column.
$scope.switchLeftColumn = function (forcePositive) {
if (!$scope.showPositives || forcePositive) {
$scope.leftDigests = positives;
negIndex = (forcePositive) ? 0 : $scope.leftIndex;
$scope.showPositives = true;
$scope.selectLeft(posIndex);
} else {
$scope.leftDigests = negatives;
posIndex = $scope.leftIndex;
$scope.showPositives = false;
$scope.selectLeft(negIndex);
}
};
// getOverlayStyle returns style values that are used to turn the
// image overlay on and off.
$scope.getOverlayStyle = function () {
if ($scope.showOverlay && $scope.currentUntriaged) {
return {
backGround: {
'background-image':
"url('" + $scope.currentUntriaged.imgUrl + "')",
padding:0,
'background-repeat': 'no-repeat',
'background-size': 'cover'
},
foreGround: {
padding: 0,
'background-blend-mode': 'multiply',
opacity: 0.9,
border: 0
}
};
}
return {};
};
function init() {
// Initialize the triage state manager.
triageStateManager = new TriageStateManager($scope, dataService);
// Initialize the variables in $scope.
$scope.testName = testName;
$scope.untriaged = [];
$scope.leftDigests = [];
positives = [];
negatives = [];
posIndex = -1;
negIndex = -1;
$scope.reloadInterval = 3;
$scope.posStats = {};
$scope.negStats = {};
$scope.posStats = null;
$scope.negStats = null;
$scope.untStats = null;
// Update the derived data.
$scope.selectUntriaged(0);
$scope.selectLeft(0);
$scope.showPositives = true;
$scope.showOverlay = true;
// Expose the constants in the template.
$scope.c = ns.c;
// Manage the URL query.
$scope.allParams = [];
$scope.query = $location.search();
// Load the data.
$scope.loadTriageData();
};
init();
}]);
/**
* skFlot implements a custom directive around Flot. We are using this
* directive as an attribute so we can set the size on the div.
**/
app.directive('skFlot', ['$window', function($window) {
var linkFn = function ($scope, element, attrs) {
var plotObj = new ns.Plot(element);
function refreshData() {
plotObj.setData($scope.data, $scope.ticks);
}
// watch the data and the ticks and redraw them as needed.
$scope.$watch('data', refreshData);
$scope.$watch('ticks', refreshData);
angular.element($window).bind('resize', function () {
plotObj.redraw();
});
};
return {
restrict: 'A',
replace: false,
scope: {
data: '=data',
ticks: '=ticks'
},
// templateUrl: 'dirtemplates/skflot.html',
link: linkFn
};
}]);
/**
* skPendingTriage implements a custom directive to provide controls to
* trigger a triage update.
**/
app.directive('skPendingTriage', [ '$timeout', function($timeout) {
var linkFn = function ($scope, element, attrs) {
$timeout(function() {
$scope.resetTriageState = $scope.resetTriageState();
$scope.saveTriageState = $scope.saveTriageState();
})
};
return {
restrict: 'E',
replace: false,
scope: {
pendingCount: '=changeCount',
triageStateDirty: '=dirty',
resetTriageState: '&resetClick',
saveTriageState: '&saveClick',
isLoggedIn: '=loggedIn'
},
templateUrl: 'templates/pending-triage.html',
link: linkFn
};
}]);
/**
* skQuery implements a custom directive to select parameter values used
* to query the backend.
**/
app.directive('skQuery', ['$timeout', function($timeout) {
var linkFn = function ($scope, element, attrs) {
$scope.isEmpty = ns.isEmpty;
$scope.$watch('outerQuery', function(newVal) {
$scope.query = angular.copy($scope.outerQuery);
});
$scope.$watch('query', function() {
for(var k in $scope.query) {
if ($scope.query.hasOwnProperty(k) && ($scope.query[k].length === 0)) {
delete $scope.query[k];
}
}
}, true);
$scope.isClean = function() {
return angular.equals($scope.query, $scope.outerQuery);
};
$scope.triggerUpdate = function() {
$scope.outerQuery = $scope.query;
// The timeout call is necessary to let outerQuery propagate.
$timeout(function(){
$scope.clickUpdate();
$scope.showFilter = false;
});
};
};
return {
restrict: 'E',
replace: false,
scope: {
params: '=allParams',
outerQuery: '=query',
clickUpdate: "&clickUpdate"
},
templateUrl: 'templates/query.html',
link: linkFn
};
}]);
/**
* skParams implements a directive to show the tables in a parameter. If the
* params-two attributes is supplied it will show both parameter sets in
* one table.
**/
app.directive('skParams', [function() {
var linkFn = function ($scope, element, attrs) {
function calcColumns() {
$scope.params = ns.getCombinedParamsTable($scope.data, $scope.dataTwo);
}
$scope.$watch("data", calcColumns);
$scope.$watch("dataTwo", calcColumns);
};
return {
restrict: 'E',
replace: true,
scope: {
data: '=params',
dataTwo: '=paramsTwo',
filterFn: '=filter'
},
templateUrl: 'templates/params-table.html',
link: linkFn
};
}]);
/**
* skImgContainer implements a directive to wrap an image with a
* classification label.
**/
app.directive('skImgContainer', [function() {
var linkFn = function ($scope, element, attrs) {
// Only wire these up if there was a digest given.
if ($scope.digest) {
// toggleStateIndicator changes the state of a digest to the 'next' state.
// This allows to iterate through all states by repeatedly clicking on
// the indicator.
$scope.toggleStateIndicator = function (digest) {
var nextState = ns.nextState($scope.triageState[$scope.testName][digest]);
$scope.setTriageState($scope.testName, digest, nextState);
};
// stateChanged helps the UI figure out if a digest has changed. This is
// used to change the background of a state indicator.
$scope.stateChanged = function (digest) {
return $scope.triageState[$scope.testName][$scope.digest] !==
$scope.initialTriageState[$scope.testName][$scope.digest];
};
}
$scope.c = ns.c;
};
return {
restrict: 'E',
replace: false,
scope: {
"digest": "=digest",
"setTriageState": "=setter",
"triageState": "=triageState",
"initialTriageState": "=initialTriageState",
"imgUrl": "=imgUrl",
"testName": "=testName"
},
transclude: true,
templateUrl: 'templates/triage-img-container.html',
link: linkFn
};
}]);
/**
* skBulkChange implements a directive to bulk change classifications.
**/
app.directive('skBulkTriage', ['$timeout', function($timeout) {
var linkFn = function ($scope, element, attrs) {
function checkChangeForOneTest() {
if ($scope.digests.length > 0) {
$scope.selected = $scope.triageState[$scope.testName][$scope.digests[0].digest];
for(var i=1, len=$scope.digests.length; i<len; i++) {
if ($scope.triageState[$scope.testName][$scope.digests[i].digest]
!== $scope.selected) {
$scope.selected = null;
break;
}
}
} else {
$scope.selected = null;
}
}
function checkChangeForAllTests() {
var init = true;
$scope.selected = null;
loop:
for(var k in $scope.triageState) {
if ($scope.triageState.hasOwnProperty(k)) {
for(var digest in $scope.triageState[k]) {
if ($scope.triageState[k].hasOwnProperty(digest)) {
if (init) {
init = false;
$scope.selected = $scope.triageState[k][digest];
}
if ($scope.triageState[k][digest] !== $scope.selected) {
$scope.selected = null;
break loop;
}
}
}
}
}
}
$scope.c = ns.c;
$timeout(function () {
$scope.setTriageState = $scope.setTriageState();
$scope.testName = $scope.testName();
if ($scope.testName) {
$scope.selectAll = $scope.setTriageState;
$scope.$watch('triageState', checkChangeForOneTest, true);
$scope.$watch('digests', checkChangeForOneTest, true);
} else {
$scope.selectAll = function(testName, digests, targetState) {
$scope.setTriageState(testName, $scope.triageState, targetState);
};
$scope.$watch('triageState', checkChangeForAllTests, true);
}
});
};
return {
restrict: 'E',
replace: true,
scope: {
"digests": "=digests",
"setTriageState": "&setter",
"triageState": "=triageState",
"testName": "&testName",
"tests": "=tests"
},
transclude: false,
templateUrl: 'templates/bulk-triage.html',
link: linkFn
};
}]);
/**
* skPagination implements a directive to show current pagination state.
**/
app.directive('skPagination', [function() {
return {
restrict: 'E',
replace: true,
scope: {
"p": "=paginator",
},
transclude: false,
templateUrl: 'templates/pagination.html'
};
}]);
/**
* skSortedTable shows an object in a sorted table.
**/
app.directive('skSortedTable', [function() {
var linkFn = function ($scope, element, attrs) {
$scope.$watch('obj', function() {
$scope.objTable = ns.getSortedObject($scope.obj);
});
};
return {
restrict: 'E',
replace: true,
scope: {
"obj": "=obj",
"hOne": "@hOne",
"hTwo": "@hTwo",
"empty": "@msg"
},
transclude: false,
templateUrl: 'templates/sorted-table.html',
link: linkFn
};
}]);
/**
* dataService provides functions to load data from the backend.
*/
app.factory('dataService', [ '$http', '$rootScope', '$interval', '$location', '$window',
function ($http, $rootScope, $interval, $location, $window) {
// Inject the logoutURL into the rootScope.
// $rootScope.logoutURL = ns.c.PREFIX_URL + ns.c.URL_LOGOUT;
$rootScope.getLogoutURL = function() {
return encodeURI(ns.c.PREFIX_URL +
ns.c.URL_LOGOUT + '?redirect=' + '/#' + $location.url());
}
$rootScope.globalStatus = null;
$rootScope.checkStatus = function () {
loadData(ns.c.URL_STATUS).then(
function (resultResp) {
$rootScope.globalStatus = resultResp;
$rootScope.corpStatus = (resultResp && resultResp.corpStatus) || {};
$rootScope.corpusList = ns.sortedKeys($rootScope.corpStatus);
if (!$rootScope.corpStatus[$rootScope.currentCorpus] &&
($rootScope.corpusList.length > 0)) {
$rootScope.currentCorpus = $rootScope.corpusList[0];
$rootScope.corpusChanged();
}
},
function (errorResp) {
console.log("Got error response for status:", errorResp);
});
};
$rootScope.getStatusString = function(corpus) {
var status = $rootScope.corpStatus[corpus].ok ?
'ok' : $rootScope.corpStatus[corpus].untriagedCount +'/'+
$rootScope.corpStatus[corpus].negativeCount;
return corpus + ' - ' +status;
};
$rootScope.corpusList = [];
$rootScope.defaultShowCorpora = ['gm', 'skp'];
$rootScope.corpusChanged = function () {
if (localStorage) {
localStorage.setItem('corpus', $rootScope.currentCorpus);
}
$rootScope.showCorpora = angular.copy($rootScope.defaultShowCorpora);
if ($rootScope.showCorpora.indexOf($rootScope.currentCorpus) == -1) {
$rootScope.showCorpora.unshift($rootScope.currentCorpus);
}
$rootScope.$broadcast('corpus-change');
};
$rootScope.forceTriage = function(corpus) {
$rootScope.currentCorpus = corpus;
$location.path("/triage");
$rootScope.corpusChanged();
}
// Load the status every 3 seconds.
$rootScope.checkStatus();
$interval($rootScope.checkStatus, 3000);
// Retrieve the corpus from localStorage if possible.
var localStorage = $window.localStorage;
$rootScope.currentCorpus = localStorage && localStorage.getItem('corpus');
if ($rootScope.currentCorpus) {
$rootScope.corpusChanged();
}
/**
* @param {string} testName if not null this will cause to fetch counts
* for a specific test.
* @param {object} query Query string to send to the backend.
* @return {Promise} Will resolve to either the data (success) or to
* the HTTP response (error).
**/
function loadData(path, query) {
var url = ns.c.PREFIX_URL + path;
// Inject the corpus selector if there is a query.
if (query && $rootScope.currentCorpus) {
query[ns.c.CORPUS_FIELD] = [$rootScope.currentCorpus];
};
return httpReq(url, 'GET', null, query).then(
function(successResp) {
var ret = successResp.data;
// Remove the corpus selector if there is a query.
if (ret && ret.query && ret.query[ns.c.CORPUS_FIELD]) {
delete ret.query[ns.c.CORPUS_FIELD];
}
return ret;
});
}
/**
* sendData sends the given data to the URL path on the backend via a
* POST request.
*
* @param {string} path relative path. The standard prefix will be added.
* @param {object} data object to be send as JSON string.
* @return {Promise} will either resolve to data (success) or an HTTP
* response object (error).
*/
function sendData(path, data) {
var url = ns.c.PREFIX_URL + path;
return httpReq(url, 'POST', data).then(
function(successResp) {
return successResp.data;
});
}
/**
* setGlobalLoginStatus injects the login status into the $rootScope
* so it is accessible throughout the application.
*/
function setGlobalLoginStatus(userId, loginURL) {
$rootScope.isLoggedIn = (userId && userId !== '');
$rootScope.userId = userId;
$rootScope.loginURL = loginURL;
}
/**
* pollLoginStatus sends a single polling request to the backend
* to determine whether the user is logged in or not.
* This corresponds to the backend implementation in 'go/login/login.go'.
*/
function pollLoginStatus() {
var url = ns.c.PREFIX_URL + ns.c.URL_LOGIN_STATUS;
httpReq(url).then(
function (posResp) {
setGlobalLoginStatus(posResp.Email, posResp.LoginURL);
},
function (errResp) {
console.log("Error:", errResp);
setGlobalLoginStatus(null, null);
});
}
// Fetch the login status immediately.
pollLoginStatus();
/**
* Make a HTTP request with the given data, method and query parameters.
*
* @param {string} url
* @param {string} method
* @param {object} data
* @param {object} queryParams
*
* @return {Promise} promise.
**/
function httpReq(url, method, data, queryParams) {
method = (method) ? method : 'GET';
var reqConfig = {
method: method,
url: url,
params: queryParams,
data: data
};
return $http(reqConfig).then(
function(successResp) {
return successResp.data;
});
}
// Interface of the service:
return {
loadData: loadData,
sendData: sendData
};
}]);
})(skia);