diff --git a/ui/css/treeherder.css b/ui/css/treeherder.css index befeec4e1..f5b88f733 100755 --- a/ui/css/treeherder.css +++ b/ui/css/treeherder.css @@ -1,12 +1,12 @@ body { - padding-top: 64px; + padding-top: 61px; padding-bottom: 500px; - min-width: 800px; + min-width: 970px; width: auto !important; width: 1300px; } -.nav, .pagination, .carousel, .panel-title a { +.pagination, .carousel, .panel-title a { cursor: pointer; } @@ -15,12 +15,16 @@ body { visibility: hidden; } +.full-height { + height: 100%; +} + .th-navbar { - min-width: 800px; + min-width: 970px; } .navbar { - min-width: 800px; + min-width: 970px; min-height: 36px; } @@ -38,9 +42,19 @@ body { .th-global-navbar { border-bottom: 1px solid black; - min-width: 800px; + min-width: 970px; } +.watched-repo-dropdown-item { + margin: 0px 10px; +} +.watched-repo-dropdown-item > a { + white-space: normal; +} + +.watched-repo-navbar { + overflow: visible; +} .th-username { margin-right: 5px; } @@ -51,14 +65,34 @@ body { .th-context-navbar { background-color: #354048; - height: 30px; - min-width: 800px; + min-width: 970px; + overflow: visible; +} + +.treeClosed { + color: rgb(161, 52, 53); +} + +.treeOpen { + color: green; +} + +.treeApproval { + color: #fb9910; +} + +.treeUnavailable { + color: lightgray; +} + +#platform-job-text-search-field { + height:28px; } .th-content { display: inline-block; overflow: hidden; - min-width: 800px; + min-width: 970px; width: 100%; padding-bottom: 101px; white-space: nowrap; @@ -76,8 +110,8 @@ body { background-color: lightgray; padding: 10px; border: 1px solid black; - max-height: 500px; - overflow: auto; + max-height: 300px; + overflow-y: auto; } .th-top-nav-options-panel .th-option-heading { @@ -149,6 +183,15 @@ body { min-width: 225px; } +.th-repo-group-items .dropdown-menu { + top: inherit; + left: inherit; +} + +.th-repo-group-items .dropdown-toggle { + cursor: pointer; +} + /** Failures */ @@ -241,8 +284,10 @@ body { .revision-link { padding-top: 5px; - width: 115px; + width: 130px; display: block; + overflow: hidden; + text-overflow: ellipsis; } .revision-button { @@ -262,6 +307,7 @@ body { .result-set .job-list-nopad { padding-left: 0; padding-right: 0; + display: block; } .selected-job { @@ -313,10 +359,14 @@ div.navbar-fixed-bottom{ } div.navbar-fixed-bottom .tab-content{ - height:160px; + height: calc(100% - 22px); overflow-y: auto; } +.bottom-panel-tabs { + height: 100%; +} + .bottom-panel-tabs .nav li a { padding: 1px 10px 2px 10px; } @@ -325,34 +375,55 @@ div.navbar-fixed-bottom dt, div.navbar-fixed-bottom dt { font-size:.8em; } -#bottom-left-panel {width:242px; position: absolute; left:5px;} -#bottom-left-panel .panel{padding:5px;} +#bottom-left-panel { + width:242px; + position: absolute; + left:5px; + bottom: 5px; +} +#bottom-left-panel .panel{ + padding:5px; +} + +#bottom-left-panel .panel-head{ + overflow: auto; + height: 100%; +} + +#bottom-left-panel .table-super-condensed { + width: 100%; +} + #bottom-center-panel { - margin-right: 90px; + margin-right: 98px; margin-left: 242px; } -#bottom-menu {position: absolute; right:-5px; width: 100px} +#bottom-menu {position: absolute; right:0; width: 104px} -.result-status-shading-success {background-color: rgba(120, 196, 192, 0.54);} -.result-status-shading-testfailed {background-color: rgba(221, 102, 2, 0.56);} -.result-status-shading-busted {background-color: rgba(144, 0, 0, 0.60);} -.result-status-shading-exception {background-color: rgba(61, 2, 85, 0.50);} -.result-status-shading-retry {background-color: #263fc3;} -.result-status-shading-usercancel {background-color: rgba(250, 115, 172, 0.63)} +.result-status-shading-success {background-color: rgba(2, 131, 44, 0.24);} +.result-status-shading-testfailed {background-color: rgba(221, 102, 2, 0.25);} +.result-status-shading-busted {background-color: rgba(144, 0, 0, 0.25);} +.result-status-shading-exception {background-color: rgba(61, 2, 85, 0.25);} +.result-status-shading-retry {background-color: rgba(38, 63, 195, 0.25);} +.result-status-shading-usercancel {background-color: rgba(250, 115, 172, 0.25)} .result-status-shading-pending {background-color: white;} .result-status-shading-running {background-color: white;} -.bottom-shadowed-panel { - height: 195px; +.bottom-shadowed-panel-with-pinboard { + height: calc(100% - 75px); +} + +.bottom-shadowed-panel-without-pinboard { + height: calc(100% - 20px); } .bottom-menu-group { - width: 90px; + width: 98px; margin-bottom: 5px; } .save-btn { - width: 73px; + width: 81px; } .save-btn-dropdown { @@ -407,12 +478,12 @@ div.navbar-fixed-bottom dt { } .pinboard-classification-comment { - width: 185px; + width: 177px; margin-top: 5px; } .pinboard-classification-select { - width: 185px; + width: 177px; } .pinned-job { @@ -427,7 +498,10 @@ div.navbar-fixed-bottom dt { margin-top: 0.3em; } -.panel-body{ padding:5px;} +.panel-body{ + padding:5px; + height: 100%; +} .timestamp-name { overflow: hidden; @@ -502,13 +576,16 @@ div.navbar-fixed-bottom dt { width: 69px; } +.click-able-icon, .nav-tabs li { + cursor: pointer; +} /** * CUSTOM BUTTONS */ .btn-view-nav { - background-color: rgba(63, 74, 81, 0.56); + background-color: rgba(75, 86, 93, 0.56); border-color: #22282d; color: lightgray; border-radius: 0; @@ -1015,7 +1092,7 @@ fieldset[disabled] .btn-repo.active { position:fixed; top:70px; right:10px; - z-index: 9000; + z-index: 1100; } #notification_box div.alert{ @@ -1044,4 +1121,6 @@ fieldset[disabled] .btn-repo.active { .form-group-inline>.form-group{ display: inline-block; vertical-align: top; -} \ No newline at end of file +} + +div.logviewer-step{ padding:6px 6px;} diff --git a/ui/help.html b/ui/help.html index 38e70e5fc..724b8d15e 100644 --- a/ui/help.html +++ b/ui/help.html @@ -54,12 +54,8 @@

Keyboard shortcuts

- - - - - - + + @@ -69,15 +65,9 @@ - - - - - - - - - + + +
spaceSelect/deselect active build or changeset
?Show the help box
cShow the comment box
sAdd selected job to the pin board
j Highlight next unstarred failure
k
p Highlight previous unstarred failure
uToggle showing only unstarred failures
ClickChoose an active build and display its details
Ctrl/Cmd-ClickSelect/deselect build or changeset
DragAdd build or changeset to comment
Ctrl/Cmd-EnterSubmit the comment form
Show only unstarred failures
Shift-ClickAdd job to the pinboard
@@ -317,4 +307,4 @@ - \ No newline at end of file + diff --git a/ui/index.html b/ui/index.html index 986061a8b..57deacbb2 100755 --- a/ui/index.html +++ b/ui/index.html @@ -13,16 +13,11 @@ - - + +
-
-
@@ -31,7 +26,7 @@ @@ -47,26 +42,39 @@ - + + + - + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -75,12 +83,14 @@ + + @@ -147,9 +157,28 @@ + + diff --git a/ui/js/app.js b/ui/js/app.js index 3400e623a..1ccc102b9 100755 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -43,18 +43,4 @@ treeherder.config(function($routeProvider, $httpProvider, $logProvider) { otherwise({redirectTo: '/jobs'}); }); - var logViewer = angular.module('logViewer',['treeherder']); -treeherder.config(function($httpProvider, $logProvider) { - // enable or disable debug messages using $log. - // comment out the next line to enable them - $logProvider.debugEnabled(false); - - // needed to avoid CORS issue when getting the logs from the ftp site - // @@@ hack for now to get it to work in the short-term - $httpProvider.defaults.useXDomain = true; - delete $httpProvider.defaults.headers.common['X-Requested-With']; - - $httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken'; - $httpProvider.defaults.xsrfCookieName = 'csrftoken'; -}); diff --git a/ui/js/config/sample.local.conf.js b/ui/js/config/sample.local.conf.js index 5f38c298a..7ce8bcac4 100644 --- a/ui/js/config/sample.local.conf.js +++ b/ui/js/config/sample.local.conf.js @@ -1,6 +1,34 @@ +'use strict'; + // mozilla hosted service //window.thServiceDomain = "http://dev.treeherder.mozilla.org"; // local vagrant instance of service window.thServiceDomain = "http://local.treeherder.mozilla.org"; +treeherder.config(['$logProvider', 'ThLogConfigProvider', + function($logProvider, ThLogConfigProvider) { + + // enable or disable debug messages using $log. + // comment out the next line to enable them + $logProvider.debugEnabled(true); + + // add classes to the blacklist. all debug messages except + // these will print + ThLogConfigProvider.setBlacklist([ +// 'thRepoDropDown', +// 'RepositoryPanelCtrl', +// 'thWatchedRepo' +// ... + ]); + + // add classes to the whitelist. Only debug messages with + // these classes will print + ThLogConfigProvider.setWhitelist([ +// 'thRepoDropDown', +// 'RepositoryPanelCtrl', +// 'thWatchedRepo', +// ... + ]); + +}]); diff --git a/ui/js/controllers/filters.js b/ui/js/controllers/filters.js index 94efa15ce..f579cc074 100644 --- a/ui/js/controllers/filters.js +++ b/ui/js/controllers/filters.js @@ -1,8 +1,10 @@ "use strict"; -treeherder.controller('StatusFilterPanelCtrl', - function StatusFilterPanelCtrl($scope, $rootScope, $routeParams, $location, $log, - localStorageService, thResultStatusList, thEvents, thJobFilters) { +treeherder.controller('FilterPanelCtrl', + function FilterPanelCtrl($scope, $rootScope, $routeParams, $location, ThLog, + localStorageService, thResultStatusList, thEvents, + thJobFilters) { + var $log = new ThLog(this.constructor.name); $scope.filterOptions = thResultStatusList; @@ -29,8 +31,10 @@ treeherder.controller('StatusFilterPanelCtrl', /** * Handle checking the "all" button for a result status group + * + * quiet - whether or not to broadcast a message about this change. */ - $scope.toggleResultStatusGroup = function(group) { + $scope.toggleResultStatusGroup = function(group, quiet) { var check = function(rs) { $scope.resultStatusFilters[rs] = group.allChecked; }; @@ -41,9 +45,35 @@ treeherder.controller('StatusFilterPanelCtrl', group.resultStatuses, group.allChecked ); - $rootScope.$broadcast(thEvents.globalFilterChanged, - {target: group, newValue: group.allChecked}); - showCheck(); + + if (!quiet) { + $rootScope.$broadcast(thEvents.globalFilterChanged, + {target: group, newValue: group.allChecked}); + } + }; + + $rootScope.$on(thEvents.showUnclassifiedFailures, function() { + $scope.showUnclassifiedFailures(); + }); + + /** + * Handle clicking the ``unclassified failures`` button. + */ + $scope.showUnclassifiedFailures = function() { + $scope.filterGroups.failures.allChecked = true; + $scope.filterGroups.nonfailures.allChecked = false; + $scope.filterGroups.inProgress.allChecked = false; + $scope.classifiedFilter = false; + $scope.unClassifiedFilter = true; + + $scope.toggleResultStatusGroup($scope.filterGroups.failures, true); + $scope.toggleResultStatusGroup($scope.filterGroups.nonfailures, true); + $scope.toggleResultStatusGroup($scope.filterGroups.inProgress, true); + + $scope.setClassificationFilter(true, $scope.classifiedFilter, true); + $scope.setClassificationFilter(false, $scope.unClassifiedFilter, true); + + $rootScope.$broadcast(thEvents.globalFilterChanged); }; /** @@ -62,7 +92,15 @@ treeherder.controller('StatusFilterPanelCtrl', } $rootScope.$broadcast(thEvents.globalFilterChanged, {target: filter, newValue: $scope.resultStatusFilters[filter]}); - showCheck(); + }; + + /** + * Toggle the filters to show either unclassified or classified jobs, + * neither or both. + */ + $scope.toggleClassificationFilter = function(isClassified) { + var isChecked = !(isClassified? $scope.classifiedFilter: $scope.unClassifiedFilter); + $scope.setClassificationFilter(isClassified, isChecked, false); }; /** @@ -72,19 +110,19 @@ treeherder.controller('StatusFilterPanelCtrl', * ``classified`` (when true) or ``unclassified`` * (when false) */ - $scope.toggleClassificationFilter = function(isClassified) { + $scope.setClassificationFilter = function(isClassified, isChecked, quiet) { var field = "failure_classification_id"; // this function is called before the checkbox value has actually // changed the scope model value, so change to the inverse. - var isChecked = !(isClassified? $scope.classifiedFilter: $scope.unClassifiedFilter); var func = isChecked? thJobFilters.addFilter: thJobFilters.removeFilter; var target = isClassified? "classified": "unclassified"; - func(field, isClassified, thJobFilters.matchType.isnull); + func(field, isClassified, thJobFilters.matchType.bool); - $rootScope.$broadcast(thEvents.globalFilterChanged, - {target: target, newValue: isChecked}); - showCheck(); + if (!quiet) { + $rootScope.$broadcast(thEvents.globalFilterChanged, + {target: target, newValue: isChecked}); + } }; $scope.createFieldFilter = function() { @@ -96,7 +134,7 @@ treeherder.controller('StatusFilterPanelCtrl', $scope.addFieldFilter = function() { - $log.debug("adding filter of " + $scope.newFieldFilter.field); + $log.debug("adding filter", $scope.newFieldFilter.field); if (!$scope.newFieldFilter || $scope.newFieldFilter.field === "" || $scope.newFieldFilter.value === "") { return; } @@ -112,7 +150,6 @@ treeherder.controller('StatusFilterPanelCtrl', $rootScope.$broadcast(thEvents.globalFilterChanged, {target: $scope.newFieldFilter.field, newValue: $scope.newFieldFilter.value}); $scope.newFieldFilter = null; - showCheck(); }; @@ -123,11 +160,10 @@ treeherder.controller('StatusFilterPanelCtrl', $rootScope.$broadcast(thEvents.globalFilterChanged, {target: "allFieldFilters", newValue: null}); $scope.fieldFilters = []; - showCheck(); }; $scope.removeFilter = function(index) { - $log.debug("removing index: " + index); + $log.debug("removing index", index); thJobFilters.removeFilter( $scope.fieldFilters[index].field, $scope.fieldFilters[index].value @@ -135,40 +171,11 @@ treeherder.controller('StatusFilterPanelCtrl', $rootScope.$broadcast(thEvents.globalFilterChanged, {target: $scope.fieldFilters[index].field, newValue: null}); $scope.fieldFilters.splice(index, 1); - showCheck(); }; - /* - @@@ TODO: CAMD: test code, remove before merge. - */ - var jobs = []; - $scope.filterGroups.inProgress.resultStatuses.forEach(function(rs) {jobs.push({ - state: rs, - result: "unknown", - failure_classification_id: null - });}); - - $scope.filterGroups.failures.resultStatuses.forEach(function(rs) {jobs.push({ - state: "completed", - result: rs, - job_type_symbol: "A", - job_type_name: "Apples", - job_group_symbol: "M", - job_group_name: "Mochitest", - failure_classification_id: "bird" - });}); - $scope.filterGroups.nonfailures.resultStatuses.forEach(function(rs) {jobs.push({ - state: "completed", - result: rs - });}); - - var showCheck = function() { - jobs.forEach(function(job) { - $log.debug("show job: " + JSON.stringify(job) + ": " + thJobFilters.showJob(job)); - }); - $log.debug(JSON.stringify(thJobFilters.getFilters())); + $scope.pinAllShownJobs = function() { + thJobFilters.pinAllShownJobs(); }; - // END test code $scope.resultStatusFilters = {}; for (var i = 0; i < $scope.filterOptions.length; i++) { diff --git a/ui/js/controllers/jobs.js b/ui/js/controllers/jobs.js index 9757da950..e1fad9f7c 100644 --- a/ui/js/controllers/jobs.js +++ b/ui/js/controllers/jobs.js @@ -1,9 +1,10 @@ "use strict"; treeherder.controller('JobsCtrl', - function JobsCtrl($scope, $http, $rootScope, $routeParams, $log, $cookies, + function JobsCtrl($scope, $http, $rootScope, $routeParams, ThLog, $cookies, localStorageService, thUrl, ThRepositoryModel, thSocket, ThResultSetModel, thResultStatusList) { + var $log = new ThLog(this.constructor.name); // load our initial set of resultsets // scope needs this function so it can be called directly by the user, too. @@ -18,8 +19,9 @@ treeherder.controller('JobsCtrl', } else { $rootScope.repoName = "mozilla-inbound"; } - ThRepositoryModel.setCurrent($rootScope.repoName); + // load the list of repos into $rootScope, and set the current repo. + ThRepositoryModel.load($scope.repoName); ThResultSetModel.addRepository($scope.repoName); @@ -28,9 +30,6 @@ treeherder.controller('JobsCtrl', $scope.job_map = ThResultSetModel.getJobMap($scope.repoName); $scope.statusList = thResultStatusList; - // load the list of repos into $rootScope, and set the current repo. - ThRepositoryModel.load($scope.repoName); - if(ThResultSetModel.isNotLoaded($scope.repoName)){ // get our first set of resultsets $scope.fetchResultSets(10); @@ -41,9 +40,11 @@ treeherder.controller('JobsCtrl', treeherder.controller('ResultSetCtrl', - function ResultSetCtrl($scope, $rootScope, $http, $log, $location, + function ResultSetCtrl($scope, $rootScope, $http, ThLog, $location, thUrl, thServiceDomain, thResultStatusInfo, - ThResultSetModel, thEvents, thJobFilters, $route) { + ThResultSetModel, thEvents, thJobFilters) { + + var $log = new ThLog(this.constructor.name); $scope.getCountClass = function(resultStatus) { return thResultStatusInfo(resultStatus).btnClass; @@ -111,8 +112,8 @@ treeherder.controller('ResultSetCtrl', thEvents.resultSetFilterChanged, $scope.resultset ); - $log.debug("toggled: " + resultStatus); - $log.debug($scope.resultStatusFilters); + $log.debug("toggled: ", resultStatus); + $log.debug("resultStatusFilters", $scope.resultStatusFilters); }; /** @@ -125,7 +126,7 @@ treeherder.controller('ResultSetCtrl', }; $scope.revisionResultsetFilterUrl = $scope.urlBasePath + "?repo=" + $scope.repoName + "&revision=" + $scope.resultset.revision; - $scope.authorResultsetFilterUrl = $scope.urlBasePath + "?repo=" + $scope.repoName + "&author=" + $scope.resultset.author; + $scope.authorResultsetFilterUrl = $scope.urlBasePath + "?repo=" + $scope.repoName + "&author=" + encodeURIComponent($scope.resultset.author); $scope.resultStatusFilters = thJobFilters.copyResultStatusFilters(); @@ -135,7 +136,7 @@ treeherder.controller('ResultSetCtrl', $scope.isCollapsedRevisions = true; $rootScope.$on(thEvents.jobContextMenu, function(event, job){ - $log.debug(thEvents.jobContextMenu + ' caught'); + $log.debug("caught", thEvents.jobContextMenu); //$scope.viewLog(job.resource_uri); }); } diff --git a/ui/js/controllers/logviewer.js b/ui/js/controllers/logviewer.js index 58bd0dc6f..b96f38c99 100644 --- a/ui/js/controllers/logviewer.js +++ b/ui/js/controllers/logviewer.js @@ -1,7 +1,9 @@ 'use strict'; logViewer.controller('LogviewerCtrl', - function Logviewer($anchorScroll, $scope, $log, $rootScope, $location, $http, $timeout, ThJobArtifactModel) { + function Logviewer($anchorScroll, $scope, ThLog, $rootScope, $location, $http, $timeout, ThJobArtifactModel) { + + var $log = new ThLog("LogviewerCtrl"); var query_string = $location.search(); if (query_string.repo !== "") { diff --git a/ui/js/controllers/main.js b/ui/js/controllers/main.js index 612521092..def4bb929 100644 --- a/ui/js/controllers/main.js +++ b/ui/js/controllers/main.js @@ -1,22 +1,49 @@ "use strict"; treeherder.controller('MainCtrl', - function MainController($scope, $rootScope, $routeParams, $location, $log, + function MainController($scope, $rootScope, $routeParams, $location, ThLog, localStorageService, ThRepositoryModel, thPinboard, - ThExclusionProfileModel, thEvents) { - $scope.query=""; - $scope.statusError = function(msg) { - $rootScope.statusMsg = msg; - $rootScope.statusColor = "red"; - }; - $scope.statusSuccess = function(msg) { - $rootScope.statusMsg = msg; - $rootScope.statusColor = "green"; - }; + thClassificationTypes, thEvents, $interval, ThExclusionProfileModel) { + + var $log = new ThLog("MainCtrl"); + + thClassificationTypes.load(); + ThRepositoryModel.load(); + $scope.clearJob = function() { // setting the selectedJob to null hides the bottom panel $rootScope.selectedJob = null; }; + $scope.processKeyboardInput = function(ev){ + + //Only listen to key commands when the body has focus. Otherwise + //html input elements won't work correctly. + if( (document.activeElement.nodeName !== 'BODY') || + (ev.keyCode === 16) ){ + return; + } + + if( (ev.keyCode === 74) || (ev.keyCode === 78) ){ + //Highlight next unclassified failure keys:j/n + $rootScope.$broadcast( + thEvents.selectNextUnclassifiedFailure + ); + + }else if( (ev.keyCode === 75) || (ev.keyCode === 80) ){ + //Highlight previous unclassified failure keys:k/p + $rootScope.$broadcast( + thEvents.selectPreviousUnclassifiedFailure + ); + + }else if(ev.keyCode === 83){ + //Select/deselect active build or changeset, keys:s + $rootScope.$broadcast(thEvents.jobPin, $rootScope.selectedJob); + + }else if(ev.keyCode === 85){ + //display only unclassified failures, keys:u + $rootScope.$broadcast(thEvents.showUnclassifiedFailures); + } + }; // detect window width and put it in scope so items can react to // a narrow/wide window @@ -30,6 +57,45 @@ treeherder.controller('MainCtrl', $scope.$apply(); }; + // the repos the user has chosen to watch + $scope.watchedRepos = ThRepositoryModel.watchedRepos; + + $scope.unwatchRepo = function(name) { + ThRepositoryModel.unwatch(name); + }; + + // update the repo status (treestatus) in an interval of every 2 minutes + $interval(ThRepositoryModel.updateAllWatchedRepoTreeStatus, 2 * 60 * 1000); + + $scope.getTopNavBarHeight = function() { + return $("#th-global-top-nav-panel").find("#top-nav-main-panel").height(); + }; + + // adjust the body padding so we can see all the job/resultset data + // if the top navbar height has changed due to window width changes + // or adding enough watched repos to wrap. + $rootScope.$watch($scope.getTopNavBarHeight, function(newValue) { + $("body").css("padding-top", newValue); + }); + + /** + * The watched repos in the nav bar can be either on the left or the + * right side of the screen and the drop-down menu may get cut off + * if it pulls right while on the left side of the screen. + * And it can change any time the user re-sized the window, so we must + * check this each time a drop-down is invoked. + */ + $scope.setDropDownPull = function(event) { + $log.debug("dropDown", event.target); + var element = event.target.offsetParent; + if (element.offsetLeft > $scope.getWidth() / 2) { + $(element).find(".dropdown-menu").addClass("pull-right"); + } else { + $(element).find(".dropdown-menu").removeClass("pull-right"); + } + + }; + // give the page a way to determine which nav toolbar to show $rootScope.$on('$locationChangeSuccess', function(ev,newUrl) { $rootScope.locationPath = $location.path().replace('/', ''); @@ -37,9 +103,6 @@ treeherder.controller('MainCtrl', $rootScope.urlBasePath = $location.absUrl().split('?')[0]; - // the repos the user has chosen to watch - $scope.watchedRepos = ThRepositoryModel.watchedRepos; - $scope.changeRepo = function(repo_name) { // hide the repo panel if they chose to load one. $scope.isRepoPanelShowing = false; diff --git a/ui/js/controllers/repository.js b/ui/js/controllers/repository.js index 0645bdb71..73a04a153 100644 --- a/ui/js/controllers/repository.js +++ b/ui/js/controllers/repository.js @@ -1,12 +1,9 @@ "use strict"; treeherder.controller('RepositoryPanelCtrl', - function RepositoryPanelCtrl($scope, $rootScope, $routeParams, $location, $log, + function RepositoryPanelCtrl($scope, $rootScope, $routeParams, $location, ThLog, localStorageService, ThRepositoryModel, thSocket) { - - $scope.saveWatchedRepos = function() { - ThRepositoryModel.saveWatchedRepos(); - }; + var $log = new ThLog(this.constructor.name); for (var repo in $scope.watchedRepos) { if($scope.watchedRepos[repo]){ @@ -14,6 +11,9 @@ treeherder.controller('RepositoryPanelCtrl', $log.debug("subscribing to "+repo+".job_failure"); } } - + $scope.toggleRepo = function(repoName) { + $scope.watchedRepos[repoName].isWatched = !$scope.watchedRepos[repoName].isWatched; + ThRepositoryModel.watchedReposUpdated(repoName); + }; } ); diff --git a/ui/js/directives/bottom_nav_panel.js b/ui/js/directives/bottom_nav_panel.js new file mode 100644 index 000000000..c282ce8f4 --- /dev/null +++ b/ui/js/directives/bottom_nav_panel.js @@ -0,0 +1,151 @@ +'use strict'; + + +treeherder.directive('thPinnedJob', function (thResultStatusInfo) { + + var getHoverText = function(job) { + var duration = Math.round((job.end_timestamp - job.start_timestamp) / 60); + var status = job.result; + if (job.state !== "completed") { + status = job.state; + } + return job.job_type_name + " - " + status + " - " + duration + "mins"; + }; + + return { + restrict: "E", + link: function(scope, element, attrs) { + var unbindWatcher = scope.$watch("job", function(newValue) { + var resultState = scope.job.result; + if (scope.job.state !== "completed") { + resultState = scope.job.state; + } + scope.job.display = thResultStatusInfo(resultState); + scope.hoverText = getHoverText(scope.job); + + if (scope.job.state === "completed") { + //Remove watchers when a job has a completed status + unbindWatcher(); + } + + }, true); + }, + templateUrl: 'partials/thPinnedJob.html' + }; +}); + +treeherder.directive('thRelatedBugSaved', function () { + + return { + restrict: "E", + templateUrl: 'partials/thRelatedBugSaved.html' + }; +}); + +treeherder.directive('thRelatedBugQueued', function () { + + return { + restrict: "E", + templateUrl: 'partials/thRelatedBugQueued.html' + }; +}); + +treeherder.directive('thFailureClassification', function ($parse, thClassificationTypes) { + return { + scope: { + failureId: "=" + }, + link: function(scope, element, attrs) { + scope.$watch('failureId', function(newVal) { + if (newVal) { + scope.classification = thClassificationTypes.classifications[newVal]; + scope.badgeColorClass=scope.classification.star; + scope.hoverText=scope.classification.name; + } + }); + }, + template: '' + + '' + + ' {{ hoverText }}' + }; +}); + +treeherder.directive('resizablePanel', function($document, ThLog) { + return { + restrict: "E", + link: function(scope, element, attr) { + var startY = 0; + var container = $(element.parent()); + + element.css({ + position: 'absolute', + cursor:'row-resize', + top:'-2px', + width: '100%', + height: '5px', + 'z-index': '100' + + }); + + element.on('mousedown', function(event) { + // Prevent default dragging of selected content + event.preventDefault(); + startY = event.pageY; + $document.on('mousemove', mousemove); + $document.on('mouseup', mouseup); + }); + + function mousemove(event) { + var y = startY - event.pageY; + startY = event.pageY; + container.height(container.height() + y); + } + + function mouseup() { + $document.unbind('mousemove', mousemove); + $document.unbind('mouseup', mouseup); + + } + + } + }; +}); + +treeherder.directive('thSimilarJobs', function(ThJobModel, ThLog){ + return { + restrict: "E", + templateUrl: "partials/similar_jobs.html", + link: function(scope, element, attr) { + scope.$watch('job', function(newVal, oldVal){ + if(newVal){ + scope.update_similar_jobs(newVal); + } + }); + scope.similar_jobs = []; + scope.similar_jobs_filters = { + "machine_id": true, + "job_type_id": true, + "build_platform_id": true + }; + scope.update_similar_jobs = function(job){ + var options = {result_set_id__ne: job.result_set_id}; + angular.forEach(scope.similar_jobs_filters, function(elem, key){ + if(elem){ + options[key] = job[key]; + } + }); + ThJobModel.get_list(options).then(function(data){ + scope.similar_jobs = data; + }); + }; + } + }; +}); + +treeherder.directive('thPinboardPanel', function(){ + return { + restrict: "E", + templateUrl: "partials/thPinboardPanel.html" + }; +}); diff --git a/ui/js/directives.js b/ui/js/directives/clonejobs.js old mode 100755 new mode 100644 similarity index 54% rename from ui/js/directives.js rename to ui/js/directives/clonejobs.js index 3c7cc77a3..71978a1b6 --- a/ui/js/directives.js +++ b/ui/js/directives/clonejobs.js @@ -2,11 +2,19 @@ /* Directives */ treeherder.directive('thCloneJobs', function( - $rootScope, $http, $log, thUrl, thCloneHtml, thServiceDomain, + $rootScope, $http, ThLog, thUrl, thCloneHtml, thServiceDomain, thResultStatusInfo, thEvents, thAggregateIds, thJobFilters, thResultStatusObject, ThResultSetModel){ - var lastJobElSelected = {}; + var $log = new ThLog("thCloneJobs"); + + var lastJobElSelected, lastJobObjSelected; + + var classificationRequired = { + "busted":1, + "exception":1, + "testfailed":1 + }; // CSS classes var btnCls = 'btn-xs'; @@ -38,15 +46,73 @@ treeherder.directive('thCloneJobs', function( }; var getHoverText = function(job) { - var duration = Math.round((job.end_timestamp - job.submit_timestamp) / 60); var jobStatus = job.result; - if (job.state != "completed") { + if (job.state !== "completed") { jobStatus = job.state; } - return job.job_type_name + " - " + jobStatus + " - " + duration + "mins"; + var result = job.job_type_name + " - " + jobStatus; + $log.debug("job timestamps", job, job.end_timestamp, job.submit_timestamp); + if (job.end_timestamp && job.submit_timestamp) { + var duration = Math.round((job.end_timestamp - job.submit_timestamp) / 60); + result = result + " - " + duration + "mins"; + } + return result; }; - var selectJob = function(el){ + //Global event listeners + $rootScope.$on( + thEvents.selectNextUnclassifiedFailure, function(ev){ + + var jobMap = ThResultSetModel.getJobMap($rootScope.repoName); + + var targetEl, jobKey; + if(!_.isEmpty(lastJobElSelected)){ + jobKey = getJobMapKey(lastJobObjSelected); + getNextUnclassifiedFailure(jobMap[jobKey].job_obj); + + }else{ + //Select the first unclassified failure + getNextUnclassifiedFailure({}); + } + }); + + $rootScope.$on( + thEvents.selectPreviousUnclassifiedFailure, function(ev){ + + var jobMap = ThResultSetModel.getJobMap($rootScope.repoName); + + var targetEl, jobKey; + if(!_.isEmpty(lastJobElSelected)){ + jobKey = getJobMapKey(lastJobObjSelected); + getPreviousUnclassifiedFailure(jobMap[jobKey].job_obj); + + }else{ + //Select the first unclassified failure + getPreviousUnclassifiedFailure({}); + } + + }); + $rootScope.$on( + thEvents.selectJob, function(ev, job){ + + selectJob(job); + + }); + + var selectJob = function(job){ + + var jobKey = getJobMapKey(job); + var jobEl = $('.' + jobKey); + + clickJobCb({}, jobEl, job); + scrollToElement(jobEl); + + lastJobElSelected = jobEl; + lastJobObjSelected = job; + + }; + + var setSelectJobStyles = function(el){ if(!_.isEmpty(lastJobElSelected)){ lastJobElSelected.removeClass(selectedBtnCls); @@ -61,7 +127,7 @@ treeherder.directive('thCloneJobs', function( }; var clickJobCb = function(ev, el, job){ - selectJob(el); + setSelectJobStyles(el); $rootScope.$broadcast(thEvents.jobClick, job); }; @@ -82,7 +148,7 @@ treeherder.directive('thCloneJobs', function( } }); } else { - $log.warn("Job had no artifacts: " + job_uri); + $log.warn("Job had no artifacts: " + job.resource_uri); } }); }; @@ -101,12 +167,12 @@ treeherder.directive('thCloneJobs', function( //Set the resultState resultState = job.result; - if (job.state != "completed") { + if (job.state !== "completed") { resultState = job.state; } resultState = resultState || 'unknown'; - if(job.job_coalesced_to_guid != null){ + if(job.job_coalesced_to_guid !== null){ // Don't count or render coalesced jobs continue; } @@ -123,25 +189,31 @@ treeherder.directive('thCloneJobs', function( //Make sure that filtering doesn't effect the resultset counts //displayed if(thJobFilters.showJob(job, resultStatusFilters) === false){ + //Keep track of visibility with this property. This + //way down stream job consumers don't need to repeatedly + //call showJob + job.visible = false; continue; } jobsShown++; + job.visible = true; + hText = getHoverText(job); key = getJobMapKey(job); jobStatus = thResultStatusInfo(resultState); - jobStatus['key'] = key; - if(parseInt(job.failure_classification_id) > 1){ - jobStatus['value'] = job.job_type_symbol + '*'; + jobStatus.key = key; + if(parseInt(job.failure_classification_id, 10) > 1){ + jobStatus.value = job.job_type_symbol + '*'; }else{ - jobStatus['value'] = job.job_type_symbol; + jobStatus.value = job.job_type_symbol; } - jobStatus['title'] = hText; - jobStatus['btnClass'] = jobStatus.btnClass; + jobStatus.title = hText; + jobStatus.btnClass = jobStatus.btnClass; jobBtn = $( jobBtnInterpolator(jobStatus) ); @@ -184,6 +256,7 @@ treeherder.directive('thCloneJobs', function( } lastJobElSelected = el; + lastJobObjSelected = job; } }; @@ -203,14 +276,14 @@ treeherder.directive('thCloneJobs', function( revision = resultset.revisions[i]; - revision['urlBasePath'] = $rootScope.urlBasePath; - revision['currentRepo'] = $rootScope.currentRepo; + revision.urlBasePath = $rootScope.urlBasePath; + revision.currentRepo = $rootScope.currentRepo; userTokens = revision.author.split(/[<>]+/); if (userTokens.length > 1) { - revision['email'] = userTokens[1]; + revision.email = userTokens[1]; } - revision['name'] = userTokens[0].trim(); + revision.name = userTokens[0].trim(); revisionHtml = revisionInterpolator(revision); ulEl.append(revisionHtml); @@ -229,7 +302,7 @@ treeherder.directive('thCloneJobs', function( var rowEl = revisionsEl.parent(); rowEl.css('display', 'block'); - if(revElDisplayState != 'block'){ + if(revElDisplayState !== 'block'){ if(jobsElDisplayState === 'block'){ toggleRevisionsSpanOnWithJobs(revisionsEl); @@ -264,7 +337,7 @@ treeherder.directive('thCloneJobs', function( var rowEl = revisionsEl.parent(); rowEl.css('display', 'block'); - if(jobsElDisplayState != 'block'){ + if(jobsElDisplayState !== 'block'){ if(revElDisplayState === 'block'){ toggleJobsSpanOnWithRevisions(jobsEl); @@ -343,7 +416,7 @@ treeherder.directive('thCloneJobs', function( jgObj = jobGroups[i]; jobsShown = 0; - if(jgObj.symbol != '?'){ + if(jgObj.symbol !== '?'){ // Job group detected, add job group symbols jobGroup = $( jobGroupInterpolator(jobGroups[i]) ); @@ -486,7 +559,7 @@ treeherder.directive('thCloneJobs', function( var jobCounts = thResultStatusObject.getResultStatusObject(); var statusKeys = _.keys(jobCounts); - jobCounts['total'] = 0; + jobCounts.total = 0; resultsetId = resultSets[i].id; @@ -511,7 +584,7 @@ treeherder.directive('thCloneJobs', function( for(k=0; k= 0; r--){ + + platforms = resultsets[r].platforms; + var p; + for(p = platforms.length - 1; p >= 0; p--){ + + groups = platforms[p].groups; + var g; + for(g = groups.length - 1; g >= 0; g--){ + + jobs = groups[g].jobs; + var j; + for(j = jobs.length - 1; j >= 0; j--){ + + if(currentJob.id === jobs[j].id){ + + //This is the current selection, get the next + startWatch = true; + continue; + } + if(startWatch){ + if( (jobs[j].visible === true) && + (classificationRequired[jobs[j].result] === 1) && + ( (parseInt(jobs[j].failure_classification_id, 10) === 1) || + (jobs[j].failure_classification_id === null) )){ + + selectJob(jobs[j]); + + //Previous test failure found + break superloop; + } + } + } + } + } + } + }; + + var scrollToElement = function(el){ + + if(el.offset() !== undefined){ + //Scroll to the job element + $('html, body').animate({ + scrollTop: el.offset().top - 250 + }, 200); + } + + }; + + var registerCustomEventCallbacks = function(scope, element, attrs){ //Register rootScope custom event listeners that require //access to the anguler level resultset scope @@ -669,6 +848,19 @@ treeherder.directive('thCloneJobs', function( } }); + + }; + + var linker = function(scope, element, attrs){ + + //Remove any jquery on() bindings + element.off(); + + //Register events callback + element.on('mousedown', _.bind(jobMouseDown, scope)); + + registerCustomEventCallbacks(scope, element, attrs); + //Clone the target html var resultsetAggregateId = thAggregateIds.getResultsetTableId( $rootScope.repoName, scope.resultset.id, scope.resultset.revision @@ -730,557 +922,11 @@ treeherder.directive('thCloneJobs', function( } element.append(targetEl); - } + }; return { link:linker, replace:true - } - -}); -treeherder.directive('thGlobalTopNavPanel', function () { - - return { - restrict: "E", - templateUrl: 'partials/thGlobalTopNavPanel.html' - }; -}); - -treeherder.directive('thWatchedRepoPanel', function () { - - return { - restrict: "E", - templateUrl: 'partials/thWatchedRepoPanel.html' - }; -}); - -treeherder.directive('thStatusFilterPanel', function () { - - return { - restrict: "E", - templateUrl: 'partials/thStatusFilterPanel.html' - }; -}); - -treeherder.directive('thRepoPanel', function () { - - return { - restrict: "E", - templateUrl: 'partials/thRepoPanel.html' - }; -}); - -treeherder.directive('thSheriffPanel', function () { - - return { - restrict: "E", - templateUrl: 'partials/thSheriffPanel.html' - }; -}); - -treeherder.directive('thSettingsPanel', function () { - - return { - restrict: "E", - templateUrl: 'partials/thSettingsPanel.html' - }; -}); - - -treeherder.directive('thFilterCheckbox', function (thResultStatusInfo) { - - return { - restrict: "E", - link: function(scope, element, attrs) { - scope.checkClass = thResultStatusInfo(scope.filterName).btnClass + "-count-classified"; - }, - templateUrl: 'partials/thFilterCheckbox.html' - }; -}); - -treeherder.directive('ngRightClick', function($parse) { - return function(scope, element, attrs) { - var fn = $parse(attrs.ngRightClick); - element.bind('contextmenu', function(event) { - scope.$apply(function() { - event.preventDefault(); - fn(scope, {$event:event}); - }); - }); - }; -}); - -treeherder.directive('thJobButton', function (thResultStatusInfo) { - - var getHoverText = function(job) { - var duration = Math.round((job.end_timestamp - job.submit_timestamp) / 60); - var status = job.result; - if (job.state != "completed") { - status = job.state; - } - return job.job_type_name + " - " + status + " - " + duration + "mins"; }; - return { - restrict: "E", - link: function(scope, element, attrs) { - var unbindWatcher = scope.$watch("job", function(newValue) { - var resultState = scope.job.result; - if (scope.job.state != "completed") { - resultState = scope.job.state; - } - scope.job.display = thResultStatusInfo(resultState); - scope.hoverText = getHoverText(scope.job); - - if (scope.job.state == "completed") { - //Remove watchers when a job has a completed status - unbindWatcher(); - } - - }, true); - }, - templateUrl: 'partials/thJobButton.html' - }; -}); - -treeherder.directive('thPinnedJob', function (thResultStatusInfo) { - - var getHoverText = function(job) { - var duration = Math.round((job.end_timestamp - job.start_timestamp) / 60); - var status = job.result; - if (job.state != "completed") { - status = job.state; - } - return job.job_type_name + " - " + status + " - " + duration + "mins"; - }; - - return { - restrict: "E", - link: function(scope, element, attrs) { - var unbindWatcher = scope.$watch("job", function(newValue) { - var resultState = scope.job.result; - if (scope.job.state != "completed") { - resultState = scope.job.state; - } - scope.job.display = thResultStatusInfo(resultState); - scope.hoverText = getHoverText(scope.job); - - if (scope.job.state == "completed") { - //Remove watchers when a job has a completed status - unbindWatcher(); - } - - }, true); - }, - templateUrl: 'partials/thPinnedJob.html' - }; -}); - -treeherder.directive('thRelatedBug', function () { - - return { - restrict: "E", - templateUrl: 'partials/thRelatedBug.html' - }; -}); - -treeherder.directive('thActionButton', function () { - - return { - restrict: "E", - templateUrl: 'partials/thActionButton.html' - }; -}); - -treeherder.directive('thResultCounts', function () { - - return { - restrict: "E", - templateUrl: 'partials/thResultCounts.html' - }; -}); - -treeherder.directive('thResultStatusCount', function () { - - return { - restrict: "E", - link: function(scope, element, attrs) { - scope.resultCountText = scope.getCountText(scope.resultStatus); - scope.resultStatusCountClassPrefix = scope.getCountClass(scope.resultStatus) - - // @@@ this will change once we have classifying implemented - scope.resultCount = scope.resultset.job_counts[scope.resultStatus]; - scope.unclassifiedResultCount = scope.resultCount; - var getCountAlertClass = function() { - if (scope.unclassifiedResultCount) { - return scope.resultStatusCountClassPrefix + "-count-unclassified"; - } else { - return scope.resultStatusCountClassPrefix + "-count-classified"; - } - } - scope.countAlertClass = getCountAlertClass(); - - scope.$watch("resultset.job_counts", function(newValue) { - scope.resultCount = scope.resultset.job_counts[scope.resultStatus]; - scope.unclassifiedResultCount = scope.resultCount; - scope.countAlertClass = getCountAlertClass(); - }, true); - - }, - templateUrl: 'partials/thResultStatusCount.html' - }; -}); - - -treeherder.directive('thAuthor', function () { - - return { - restrict: "E", - link: function(scope, element, attrs) { - var userTokens = attrs.author.split(/[<>]+/); - var email = ""; - if (userTokens.length > 1) { - email = userTokens[1]; - } - scope.authorName = userTokens[0].trim(); - scope.authorEmail = email; - }, - template: '' + - '{{authorName}}' - }; -}); - - -// allow an input on a form to request focus when the value it sets in its -// ``focus-me`` directive is true. You can set ``focus-me="focusInput"`` and -// when ``$scope.focusInput`` changes to true, it will request focus on -// the element with this directive. -treeherder.directive('focusMe', function($timeout) { - return { - link: function(scope, element, attrs) { - scope.$watch(attrs.focusMe, function(value) { - if(value === true) { - $timeout(function() { - element[0].focus(); - scope[attrs.focusMe] = false; - }, 0); - } - }); - } - }; -}); - -treeherder.directive('thStar', function ($parse, thClassificationTypes) { - return { - scope: { - starId: "=" - }, - link: function(scope, element, attrs) { - scope.$watch('starId', function(newVal) { - if (newVal !== undefined) { - scope.starType = thClassificationTypes[newVal]; - scope.badgeColorClass=scope.starType.star; - scope.hoverText=scope.starType.name; - } - }); - }, - template: '' + - '' + - ' {{ hoverText }}' - }; -}); - -treeherder.directive('thShowJobs', function ($parse, thResultStatusInfo) { - return { - link: function(scope, element, attrs) { - scope.$watch('resultSeverity', function(newVal) { - if (newVal) { - var rsInfo = thResultStatusInfo(newVal) - scope.resultsetStateBtn = rsInfo.btnClass; - scope.icon = rsInfo.showButtonIcon; - } - }); - }, - template: '' + - ' ' + - '{{ \' jobs\' | showOrHide:isCollapsedResults }}' - }; -}); - -treeherder.directive('thRevision', function($parse) { - - return { - restrict: "E", - link: function(scope, element, attrs) { - scope.$watch('resultset.revisions', function(newVal) { - if (newVal) { - scope.revisionUrl = scope.currentRepo.url + "/rev/" + scope.revision.revision; - } - }, true); - }, - templateUrl: 'partials/thRevision.html' - }; -}); - - -treeherder.directive('resizablePanel', function($document, $log) { - return { - restrict: "E", - link: function(scope, element, attr) { - var startY = 0 - var container = $(element.parent()); - - element.css({ - position: 'absolute', - cursor:'row-resize', - top:'-2px', - width: '100%', - height: '5px', - 'z-index': '100' - - }); - - element.on('mousedown', function(event) { - // Prevent default dragging of selected content - event.preventDefault(); - startY = event.pageY; - $document.on('mousemove', mousemove); - $document.on('mouseup', mouseup); - }); - - function mousemove(event) { - var y = startY - event.pageY; - startY = event.pageY; - container.height(container.height() + y); - - } - - function mouseup() { - $document.unbind('mousemove', mousemove); - $document.unbind('mouseup', mouseup); - - } - - } - }; -}); - -treeherder.directive('personaButtons', function($http, $q, $log, $rootScope, localStorageService, - thServiceDomain, BrowserId, ThUserModel) { - - return { - restrict: "E", - link: function(scope, element, attrs) { - scope.user = scope.user - || angular.fromJson(localStorageService.get('user')) - || {}; - // check if already know who the current user is - // if the user.email value is null, it means that he's not logged in - scope.user.email = scope.user.email || null; - scope.user.loggedin = scope.user.email == null ? false : true; - - scope.login = function(){ - /* - * BrowserID.login returns a promise of the verification. - * If successful, we will find the user email in the response - */ - BrowserId.login() - .then(function(response){ - scope.user.loggedin = true; - scope.user.email = response.data.email; - // retrieve the current user's info from the api - // including the exclusion profile - ThUserModel.get().then(function(user){ - angular.extend(scope.user, user); - localStorageService.add('user', angular.toJson(scope.user)); - }, null); - },function(){ - // logout if the verification failed - scope.logout(); - }); - }; - scope.logout = function(){ - BrowserId.logout().then(function(response){ - scope.user = {loggedin: false, email:null}; - localStorageService.remove('user'); - }); - }; - - - navigator.id.watch({ - /* - * loggedinUser is all that we know about the user before - * the interaction with persona. This value could come from a cookie to persist the authentication - * among page reloads. If the value is null, the user is considered logged out. - */ - - loggedInUser: scope.user.email, - /* - * We need a watch call to interact with persona. - * onLogin is called when persona provides an assertion - * This is the only way we can know the assertion from persona, - * so we resolve BrowserId.requestDeferred with the assertion retrieved - */ - onlogin: function(assertion){ - if (BrowserId.requestDeferred) { - BrowserId.requestDeferred.resolve(assertion); - } - }, - - /* - * Resolve BrowserId.logoutDeferred once the user is logged out from persona - */ - onlogout: function(){ - if (BrowserId.logoutDeferred) { - BrowserId.logoutDeferred.resolve(); - } - } - }); - }, - templateUrl: 'partials/persona_buttons.html' - }; -}); - -treeherder.directive('thSimilarJobs', function(ThJobModel, $log){ - return { - restrict: "E", - templateUrl: "partials/similar_jobs.html", - link: function(scope, element, attr) { - scope.$watch('job', function(newVal, oldVal){ - if(newVal){ - scope.update_similar_jobs(newVal); - } - }); - scope.similar_jobs = [] - scope.similar_jobs_filters = { - "machine_id": true, - "job_type_id": true, - "build_platform_id": true - } - scope.update_similar_jobs = function(job){ - var options = {result_set_id__ne: job.result_set_id}; - angular.forEach(scope.similar_jobs_filters, function(elem, key){ - if(elem){ - options[key] = job[key]; - } - }); - ThJobModel.get_list(options).then(function(data){ - scope.similar_jobs = data; - }); - }; - } - } -}); - -treeherder.directive('thNotificationBox', function($log, thNotify){ - return { - restrict: "E", - templateUrl: "partials/thNotificationsBox.html", - link: function(scope, element, attr) { - scope.notifier = thNotify - scope.alert_class_prefix = "alert-" - } - } -}); - -treeherder.directive('numbersOnly', function(){ - return { - require: 'ngModel', - link: function(scope, element, attrs, modelCtrl) { - modelCtrl.$parsers.push(function (inputValue) { - // this next is necessary for when using ng-required on your input. - // In such cases, when a letter is typed first, this parser will be called - // again, and the 2nd time, the value will be undefined - if (inputValue == undefined) return '' - var transformedInput = inputValue.replace(/[^0-9]/g, ''); - if (transformedInput!=inputValue) { - modelCtrl.$setViewValue(transformedInput); - modelCtrl.$render(); - } - - return transformedInput; - }); - } - }; -}); - -treeherder.directive('thPinboardPanel', function(){ - return { - restrict: "E", - templateUrl: "partials/thPinboardPanel.html" - } -}); - -treeherder.directive("thMultiSelect", function($log){ - return { - restrict: "E", - templateUrl: "partials/thMultiSelect.html", - scope: { - leftList: "=", - rightList: "=" - }, - link: function(scope, element, attrs){ - - scope.leftSelected = []; - scope.rightSelected = []; - // move the elements selected from one list to the other - var move_options = function(what, from, to){ - var found; - for(var i=0;i 0){ - $(element[0]).append( - $("") - .attr("title", elem_list_clone.join(", ")) - .text(" and "+ elem_list_clone.length+ " others") - .tooltip() - ); - } - } - }); - } - } }); diff --git a/ui/js/directives/main.js b/ui/js/directives/main.js new file mode 100755 index 000000000..d6ec4b83d --- /dev/null +++ b/ui/js/directives/main.js @@ -0,0 +1,135 @@ +'use strict'; + +treeherder.directive('ngRightClick', function($parse) { + return function(scope, element, attrs) { + var fn = $parse(attrs.ngRightClick); + element.bind('contextmenu', function(event) { + scope.$apply(function() { + event.preventDefault(); + fn(scope, {$event:event}); + }); + }); + }; +}); + +// allow an input on a form to request focus when the value it sets in its +// ``focus-me`` directive is true. You can set ``focus-me="focusInput"`` and +// when ``$scope.focusInput`` changes to true, it will request focus on +// the element with this directive. +treeherder.directive('focusMe', function($timeout) { + return { + link: function(scope, element, attrs) { + scope.$watch(attrs.focusMe, function(value) { + if(value === true) { + $timeout(function() { + element[0].focus(); + scope[attrs.focusMe] = false; + }, 0); + } + }); + } + }; +}); + +treeherder.directive('thNotificationBox', function(thNotify){ + return { + restrict: "E", + templateUrl: "partials/thNotificationsBox.html", + link: function(scope, element, attr) { + scope.notifier = thNotify + scope.alert_class_prefix = "alert-" + } + } +}); + +treeherder.directive('numbersOnly', function(){ + return { + require: 'ngModel', + link: function(scope, element, attrs, modelCtrl) { + modelCtrl.$parsers.push(function (inputValue) { + // this next is necessary for when using ng-required on your input. + // In such cases, when a letter is typed first, this parser will be called + // again, and the 2nd time, the value will be undefined + if (inputValue == undefined) return '' + var transformedInput = inputValue.replace(/[^0-9]/g, ''); + if (transformedInput!=inputValue) { + modelCtrl.$setViewValue(transformedInput); + modelCtrl.$render(); + } + + return transformedInput; + }); + } + }; +}); + +treeherder.directive("thMultiSelect", function($log){ + return { + restrict: "E", + templateUrl: "partials/thMultiSelect.html", + scope: { + leftList: "=", + rightList: "=" + }, + link: function(scope, element, attrs){ + + scope.leftSelected = []; + scope.rightSelected = []; + // move the elements selected from one list to the other + var move_options = function(what, from, to){ + var found; + for(var i=0;i 0){ + $(element[0]).append( + $("") + .attr("title", elem_list_clone.join(", ")) + .text(" and "+ elem_list_clone.length+ " others") + .tooltip() + ); + } + } + }); + } + } +}); diff --git a/ui/js/directives/persona.js b/ui/js/directives/persona.js new file mode 100644 index 000000000..41f485e96 --- /dev/null +++ b/ui/js/directives/persona.js @@ -0,0 +1,71 @@ +'use strict'; + +treeherder.directive('personaButtons', function($http, $q, ThLog, $rootScope, localStorageService, thServiceDomain, BrowserId) { + + return { + restrict: "E", + link: function(scope, element, attrs) { + scope.user = scope.user || {}; + // check if already know who the current user is + // if the user.email value is null, it means that he's not logged in + scope.user.email = scope.user.email || localStorageService.get('user.email'); + scope.user.loggedin = scope.user.email === null ? false : true; + + scope.login = function(){ + /* + * BrowserID.login returns a promise of the verification. + * If successful, we will find the user email in the response + */ + BrowserId.login() + .then(function(response){ + scope.user.loggedin = true; + scope.user.email = response.data.email; + localStorageService.add('user.email', scope.user.email); + },function(){ + // logout if the verification failed + scope.logout(); + }); + }; + scope.logout = function(){ + BrowserId.logout().then(function(response){ + scope.user.loggedin = false; + scope.user.email = null; + localStorageService.remove('user.loggedin'); + localStorageService.remove('user.email'); + }); + }; + + + navigator.id.watch({ + /* + * loggedinUser is all that we know about the user before + * the interaction with persona. This value could come from a cookie to persist the authentication + * among page reloads. If the value is null, the user is considered logged out. + */ + + loggedInUser: scope.user.email, + /* + * We need a watch call to interact with persona. + * onLogin is called when persona provides an assertion + * This is the only way we can know the assertion from persona, + * so we resolve BrowserId.requestDeferred with the assertion retrieved + */ + onlogin: function(assertion){ + if (BrowserId.requestDeferred) { + BrowserId.requestDeferred.resolve(assertion); + } + }, + + /* + * Resolve BrowserId.logoutDeferred once the user is logged out from persona + */ + onlogout: function(){ + if (BrowserId.logoutDeferred) { + BrowserId.logoutDeferred.resolve(); + } + } + }); + }, + templateUrl: 'partials/persona_buttons.html' + }; +}); diff --git a/ui/js/directives/resultsets.js b/ui/js/directives/resultsets.js new file mode 100644 index 000000000..45edfe023 --- /dev/null +++ b/ui/js/directives/resultsets.js @@ -0,0 +1,83 @@ +'use strict'; + +treeherder.directive('thActionButton', function () { + + return { + restrict: "E", + templateUrl: 'partials/thActionButton.html' + }; +}); + +treeherder.directive('thResultCounts', function () { + + return { + restrict: "E", + templateUrl: 'partials/thResultCounts.html' + }; +}); + +treeherder.directive('thResultStatusCount', function () { + + return { + restrict: "E", + link: function(scope, element, attrs) { + scope.resultCountText = scope.getCountText(scope.resultStatus); + scope.resultStatusCountClassPrefix = scope.getCountClass(scope.resultStatus); + + // @@@ this will change once we have classifying implemented + scope.resultCount = scope.resultset.job_counts[scope.resultStatus]; + scope.unclassifiedResultCount = scope.resultCount; + var getCountAlertClass = function() { + if (scope.unclassifiedResultCount) { + return scope.resultStatusCountClassPrefix + "-count-unclassified"; + } else { + return scope.resultStatusCountClassPrefix + "-count-classified"; + } + }; + scope.countAlertClass = getCountAlertClass(); + + scope.$watch("resultset.job_counts", function(newValue) { + scope.resultCount = scope.resultset.job_counts[scope.resultStatus]; + scope.unclassifiedResultCount = scope.resultCount; + scope.countAlertClass = getCountAlertClass(); + }, true); + + }, + templateUrl: 'partials/thResultStatusCount.html' + }; +}); + +treeherder.directive('thRevision', function($parse) { + + return { + restrict: "E", + link: function(scope, element, attrs) { + scope.$watch('resultset.revisions', function(newVal) { + if (newVal) { + scope.revisionUrl = scope.currentRepo.url + "/rev/" + scope.revision.revision; + } + }, true); + }, + templateUrl: 'partials/thRevision.html' + }; +}); + +treeherder.directive('thAuthor', function () { + + return { + restrict: "E", + link: function(scope, element, attrs) { + var userTokens = attrs.author.split(/[<>]+/); + var email = ""; + if (userTokens.length > 1) { + email = userTokens[1]; + } + scope.authorName = userTokens[0].trim(); + scope.authorEmail = email; + }, + template: '' + + '{{authorName}}' + }; +}); + diff --git a/ui/js/directives/top_nav_bar.js b/ui/js/directives/top_nav_bar.js new file mode 100644 index 000000000..22c39f738 --- /dev/null +++ b/ui/js/directives/top_nav_bar.js @@ -0,0 +1,80 @@ +'use strict'; + +treeherder.directive('thFilterCheckbox', function (thResultStatusInfo) { + + return { + restrict: "E", + link: function(scope, element, attrs) { + scope.checkClass = thResultStatusInfo(scope.filterName).btnClass + "-count-classified"; + }, + templateUrl: 'partials/thFilterCheckbox.html' + }; +}); + +treeherder.directive('thWatchedRepo', function (ThLog, ThRepositoryModel) { + var $log = new ThLog("thWatchedRepo"); + + var statusInfo = { + "open": { + icon: "fa-circle-o", + color: "treeOpen" + }, + "approval required": { + icon: "fa-lock", + color: "treeApproval" + }, + "closed": { + icon: "fa-times-circle", + color: "treeClosed" + }, + "unavailable": { + icon: "fa-chain-broken", + color: "treeUnavailable" + } + }; + + return { + restrict: "E", + link: function(scope, element, attrs) { + + scope.$watch('repoData.treeStatus', function(newVal) { + if (newVal) { + $log.debug("updated treeStatus", newVal.status); + scope.statusIcon = statusInfo[newVal.status].icon; + scope.statusColor = statusInfo[newVal.status].color; + scope.titleText = newVal.status; + if (newVal.message_of_the_day) { + scope.titleText = scope.titleText + ' - ' + newVal.message_of_the_day; + } + } + }, true); + + }, + templateUrl: 'partials/thWatchedRepo.html' + }; +}); + +treeherder.directive('thRepoDropDown', function (ThLog, ThRepositoryModel) { + var $log = new ThLog("thRepoDropDown"); + + return { + restrict: "E", + replace: true, + link: function(scope, element, attrs) { + + scope.name = attrs.name; + var repo_obj = ThRepositoryModel.getRepo(attrs.name); + scope.pushlog = repo_obj.url +"/pushloghtml"; + + scope.$watch('repoData.treeStatus', function(newVal) { + if (newVal) { + $log.debug("updated treeStatus", repo_obj, newVal); + scope.reason = newVal.reason; + scope.message_of_the_day = newVal.message_of_the_day; + } + }, true); + + }, + templateUrl: 'partials/thRepoDropDown.html' + }; +}); diff --git a/ui/js/filters.js b/ui/js/filters.js index 0301f7b02..3ddacb39b 100755 --- a/ui/js/filters.js +++ b/ui/js/filters.js @@ -24,4 +24,12 @@ treeherder.filter('platformName', function() { // if it's not found in Config.js, then return it unchanged. return name; }; -}) \ No newline at end of file +}) + +treeherder.filter('stripHtml', function() { + return function(input) { + var str = input || ''; + return str.replace(/<\/?[^>]+>/gi, ''); + }; +}) + diff --git a/ui/js/services/models/bug_job_map.js b/ui/js/models/bug_job_map.js similarity index 96% rename from ui/js/services/models/bug_job_map.js rename to ui/js/models/bug_job_map.js index 5c344b185..b81418502 100644 --- a/ui/js/services/models/bug_job_map.js +++ b/ui/js/models/bug_job_map.js @@ -38,7 +38,7 @@ treeherder.factory('ThBugJobMapModel', function($http, thUrl) { // an instance method to delete a ThBugJobMap object ThBugJobMapModel.prototype.delete = function(){ var pk = this.job_id+"-"+this.bug_id; - return $http.delete(ThBugJobMapModel.get_uri()+pk); + return $http.delete(ThBugJobMapModel.get_uri()+pk+"/"); }; return ThBugJobMapModel; diff --git a/ui/js/services/models/build_platform.js b/ui/js/models/build_platform.js similarity index 100% rename from ui/js/services/models/build_platform.js rename to ui/js/models/build_platform.js diff --git a/ui/js/services/models/classification.js b/ui/js/models/classification.js similarity index 94% rename from ui/js/services/models/classification.js rename to ui/js/models/classification.js index 52585b329..708a82090 100644 --- a/ui/js/services/models/classification.js +++ b/ui/js/models/classification.js @@ -1,6 +1,6 @@ 'use strict'; -treeherder.factory('ThJobClassificationModel', function($http, $log, thUrl) { +treeherder.factory('ThJobClassificationModel', function($http, ThLog, thUrl) { // ThJobClassificationModel is the js counterpart of note var ThJobClassificationModel = function(data) { @@ -39,7 +39,7 @@ treeherder.factory('ThJobClassificationModel', function($http, $log, thUrl) { // an instance method to delete a ThJobClassificationModel object ThJobClassificationModel.prototype.delete = function(){ - return $http.delete(ThJobClassificationModel.get_uri()+this.id); + return $http.delete(ThJobClassificationModel.get_uri()+this.id+"/"); }; return ThJobClassificationModel; diff --git a/ui/js/services/models/exclusion_profile.js b/ui/js/models/exclusion_profile.js similarity index 100% rename from ui/js/services/models/exclusion_profile.js rename to ui/js/models/exclusion_profile.js diff --git a/ui/js/services/models/job.js b/ui/js/models/job.js similarity index 80% rename from ui/js/services/models/job.js rename to ui/js/models/job.js index 1e76fe0a5..3723b4a1b 100644 --- a/ui/js/services/models/job.js +++ b/ui/js/models/job.js @@ -1,7 +1,7 @@ 'use strict'; -treeherder.factory('ThJobModel', ['$http', '$log', 'thUrl', function($http, $log, thUrl) { - // ThJobArtifactModel is the js counterpart of job_artifact +treeherder.factory('ThJobModel', function($http, ThLog, thUrl) { + // ThJobModel is the js counterpart of job var ThJobModel = function(data) { // creates a new instance of ThJobArtifactModel @@ -26,10 +26,10 @@ treeherder.factory('ThJobModel', ['$http', '$log', 'thUrl', function($http, $log ThJobModel.get = function(pk) { // a static method to retrieve a single instance of ThJobModel - return $http.get(ThJobModel.get_uri()+pk).then(function(response) { + return $http.get(ThJobModel.get_uri()+pk+"/").then(function(response) { return new ThJobModel(response.data); }); }; return ThJobModel; -}]); +}); diff --git a/ui/js/services/models/job_artifact.js b/ui/js/models/job_artifact.js similarity index 92% rename from ui/js/services/models/job_artifact.js rename to ui/js/models/job_artifact.js index fcd2d2a10..6d862faed 100644 --- a/ui/js/services/models/job_artifact.js +++ b/ui/js/models/job_artifact.js @@ -1,6 +1,6 @@ 'use strict'; -treeherder.factory('ThJobArtifactModel', ['$http', '$log', 'thUrl', function($http, $log, thUrl) { +treeherder.factory('ThJobArtifactModel', function($http, ThLog, thUrl) { // ThJobArtifactModel is the js counterpart of job_artifact var ThJobArtifactModel = function(data) { @@ -32,4 +32,4 @@ treeherder.factory('ThJobArtifactModel', ['$http', '$log', 'thUrl', function($ht }; return ThJobArtifactModel; -}]); \ No newline at end of file +}); \ No newline at end of file diff --git a/ui/js/services/models/job_filter.js b/ui/js/models/job_filter.js similarity index 100% rename from ui/js/services/models/job_filter.js rename to ui/js/models/job_filter.js diff --git a/ui/js/services/models/job_type.js b/ui/js/models/job_type.js similarity index 100% rename from ui/js/services/models/job_type.js rename to ui/js/models/job_type.js diff --git a/ui/js/services/models/option.js b/ui/js/models/option.js similarity index 100% rename from ui/js/services/models/option.js rename to ui/js/models/option.js diff --git a/ui/js/models/repository.js b/ui/js/models/repository.js new file mode 100644 index 000000000..394b3197f --- /dev/null +++ b/ui/js/models/repository.js @@ -0,0 +1,185 @@ +'use strict'; + +treeherder.factory('ThRepositoryModel', + function($http, thUrl, $rootScope, ThLog, localStorageService, + thSocket, treeStatus) { + var $log = new ThLog("ThRepositoryModel"); + + var new_failures = {}; + var repos = {}; + + thSocket.on('job_failure', function(msg){ + if (! new_failures.hasOwnProperty(msg.branch)){ + new_failures[msg.branch] = []; + } + new_failures[msg.branch].push(msg.id); + $log.debug("new failure on branch ", msg.branch); + }); + + // get the repositories (aka trees) + // sample: 'resources/menu.json' + var getByName = function(name) { + if ($rootScope.repos !== undefined) { + for (var i = 0; i < $rootScope.repos.length; i++) { + var repo = $rootScope.repos[i]; + if (repo.name === name) { + return repo; + } + } + } else { + $log.warn("Repos list has not been loaded."); + } + $log.warn("'" + name + "' not found in repos list."); + return null; + }; + + + // get by category + var getByGroup = function() { + var groupedRepos = {}; + var group = function(repo) { + if (!_.has(groupedRepos, repo.repository_group.name)) { + groupedRepos[repo.repository_group.name] = []; + } + groupedRepos[repo.repository_group.name].push(repo); + }; + + if (!groupedRepos.length) { + _.each($rootScope.repos, group); + } + return groupedRepos; + }; + + var addAsUnwatched = function(repo) { + repos[repo.name] = { + isWatched: false, + treeStatus: null, + unclassifiedFailureCount: 0 + }; + }; + + /** + * We want to add this repo as watched, but we also + * want to get the treestatus for it + */ + var addAsWatched = function(data, repoName) { + if (data.isWatched) { + repos[repoName] = { + isWatched: true, + treeStatus: null, + unclassifiedFailureCount: 0 + }; + updateTreeStatus(repoName); + $log.debug("watchedRepo", repoName, repos[repoName]); + } + }; + + var unwatch = function(name) { + if (!_.contains(repos, name)) { + repos[name].isWatched = false; + } + watchedReposUpdated(); + }; + + var get_uri = function(){ + return thUrl.getRootUrl("/repository/"); + } + + var get_list = function(){ + return $http.get(api.get_uri(), {cache: true}) + } + + var load = function(name) { + + var storedWatchedRepos = localStorageService.get("watchedRepos"); + + return get_list(). + success(function(data) { + $rootScope.repos = data; + $rootScope.groupedRepos = getByGroup(); + + _.each(data, addAsUnwatched); + if (storedWatchedRepos) { + _.each(storedWatchedRepos, addAsWatched); + } + localStorageService.add("watchedRepos", repos); + + if (name) { + $rootScope.currentRepo = getByName(name); + addAsWatched({isWatched: true}, name); + } + watchedReposUpdated(); + }); + }; + + + var getCurrent = function() { + return $rootScope.currentRepo; + }; + + var setCurrent = function(name) { + $rootScope.currentRepo = getByName(name); + $log.debug("repoModel", "setCurrent", name, "watchedRepos", repos); + }; + + var repo_has_failures = function(repo_name){ + return ($rootScope.new_failures.hasOwnProperty(repo_name) && + $rootScope.new_failures[repo_name].length > 0); + }; + + var watchedReposUpdated = function(repoName) { + localStorageService.add("watchedRepos", repos); + if (repoName) { + updateTreeStatus(repoName); + } else { + updateAllWatchedRepoTreeStatus(); + } + }; + + var updateTreeStatus = function(repoName) { + if (repos[repoName].isWatched) { + $log.debug("updateTreeStatus", "updating", repoName); + treeStatus.get(repoName).then(function(data) { + repos[repoName].treeStatus = data.data; + }, function(data) { + repos[repoName].treeStatus = { + status: "unavailable", + message_of_the_day: repoName + + ' is not supported in treestatus.mozilla.org' + }; + }); + } + }; + + var updateAllWatchedRepoTreeStatus = function() { + _.each(_.keys(repos), updateTreeStatus); + }; + + + return { + // load the list of repos into $rootScope, and set the current repo. + load: load, + + // return the currently selected repo + getCurrent: getCurrent, + + // set the current repo to one in the repos list + setCurrent: setCurrent, + + // get a repo object without setting anything + getRepo: getByName, + + getByGroup: getByGroup, + + watchedRepos: repos, + + watchedReposUpdated: watchedReposUpdated, + + unwatch: unwatch, + + updateAllWatchedRepoTreeStatus: updateAllWatchedRepoTreeStatus, + + repo_has_failures: repo_has_failures + + }; +}); diff --git a/ui/js/services/models/resultsets.js b/ui/js/models/resultsets.js similarity index 90% rename from ui/js/services/models/resultsets.js rename to ui/js/models/resultsets.js index b1219f8a6..0f83c6dc4 100644 --- a/ui/js/services/models/resultsets.js +++ b/ui/js/models/resultsets.js @@ -1,10 +1,12 @@ 'use strict'; treeherder.factory('ThResultSetModel', - ['$log', '$rootScope', 'thResultSets', 'thSocket', - 'ThJobModel', 'thEvents', 'thAggregateIds', - function($log, $rootScope, thResultSets, thSocket, - ThJobModel, thEvents, thAggregateIds) { + ['$rootScope', 'thResultSets', 'thSocket', + 'ThJobModel', 'thEvents', 'thAggregateIds', 'ThLog', + function($rootScope, thResultSets, thSocket, + ThJobModel, thEvents, thAggregateIds, ThLog) { + + var $log = new ThLog("ThResultSetModel"); /****** * Handle updating the resultset datamodel based on a queue of jobs @@ -112,7 +114,7 @@ treeherder.factory('ThResultSetModel', var getPlatformKey = function(name, option){ var key = name; - if(option != undefined){ + if(option !== undefined){ key += option; } return key; @@ -144,6 +146,13 @@ treeherder.factory('ThResultSetModel', repositories[repoName].rsMapOldestTimestamp = rs_obj.push_timestamp; } + //Keep track of the last resultset id for paging + var resultsetId = parseInt(rs_obj.id, 10); + if( (resultsetId < repositories[repoName].rsOffsetId) || + (repositories[repoName].rsOffsetId === 0) ){ + repositories[repoName].rsOffsetId = resultsetId; + } + // platforms for (var pl_i = 0; pl_i < rs_obj.platforms.length; pl_i++) { var pl_obj = rs_obj.platforms[pl_i]; @@ -191,10 +200,9 @@ treeherder.factory('ThResultSetModel', repositories[repoName].resultSets.sort(rsCompare); - $log.debug("oldest job: " + repositories[repoName].jobMapOldestId); - $log.debug("oldest result set: " + repositories[repoName].rsMapOldestTimestamp); - $log.debug("done mapping:"); - $log.debug(repositories[repoName].rsMap); + $log.debug("oldest job: ", repositories[repoName].jobMapOldestId); + $log.debug("oldest result set: ", repositories[repoName].rsMapOldestTimestamp); + $log.debug("done mapping:", repositories[repoName].rsMap); }; /** @@ -226,7 +234,7 @@ treeherder.factory('ThResultSetModel', var pl_obj = { name: newJob.platform, - option: newJob.platform_opt, + option: newJob.platform_option, groups: [] }; @@ -308,28 +316,31 @@ treeherder.factory('ThResultSetModel', } if (jobFetchList.length > 0) { - $log.debug("processing jobFetchList"); - $log.debug(jobFetchList); + $log.debug("processing jobFetchList", jobFetchList); // make an ajax call to get the job details - - ThJobModel.get_list({ - id__in: jobFetchList.join() - }).then( - _.bind(updateJobs, $rootScope, repoName), - function(data) { - $log.error("Error fetching jobUpdateQueue: " + data); - }); + fetchJobs(repoName, jobFetchList); } }; + /** + * Fetch the job objects for the ids in ``jobFetchList`` and update them + * in the data model. + */ + var fetchJobs = function(repoName, jobFetchList) { + ThJobModel.get_list({ + id__in: jobFetchList.join() + }).then( + _.bind(updateJobs, $rootScope, repoName), + function(data) { + $log.error("Error fetching jobs: " + data); + }); + }; var aggregateJobPlatform = function(repoName, job, platformData){ var resultsetId, platformName, platformOption, platformAggregateId, platformKey, jobUpdated, resultsetAggregateId, revision, jobGroups; - console.log('aggregating job platform'); - console.log(job); jobUpdated = updateJob(repoName, job); //the job was not updated or added to the model, don't include it @@ -340,7 +351,7 @@ treeherder.factory('ThResultSetModel', resultsetId = job.result_set_id; platformName = job.platform; - platformOption = job.platform_opt; + platformOption = job.platform_option; if(_.isEmpty(repositories[repoName].rsMap[ resultsetId ])){ //We don't have this resultset @@ -351,7 +362,7 @@ treeherder.factory('ThResultSetModel', repoName, job.result_set_id, job.platform, - job.platform_opt + job.platform_option ); if(!platformData[platformAggregateId]){ @@ -366,6 +377,7 @@ treeherder.factory('ThResultSetModel', platformKey = getPlatformKey(platformName, platformOption); + $log.debug("aggregateJobPlatform", repoName, resultsetId, platformKey, repositories); jobGroups = repositories[repoName].rsMap[resultsetId].platforms[platformKey].pl_obj.groups; platformData[platformAggregateId] = { platformName:platformName, @@ -389,7 +401,7 @@ treeherder.factory('ThResultSetModel', */ var updateJobs = function(repoName, jobList) { - $log.debug("number of jobs returned for add/update: " + jobList.length); + $log.debug("number of jobs returned for add/update: ", jobList.length); var platformData = {}; @@ -450,11 +462,11 @@ treeherder.factory('ThResultSetModel', } if (loadedJob) { - $log.debug("updating existing job"); + $log.debug("updating existing job", loadedJob, newJob); _.extend(loadedJob, newJob); } else { // this job is not yet in the model or the map. add it to both - $log.debug("adding new job"); + $log.debug("adding new job", newJob); var grpMapElement = getOrCreateGroup(repoName, newJob); @@ -486,7 +498,7 @@ treeherder.factory('ThResultSetModel', var added = []; for (var i = data.length - 1; i > -1; i--) { if (data[i].push_timestamp > repositories[repoName].rsMapOldestTimestamp) { - $log.debug("prepending resultset: " + data[i].id); + $log.debug("prepending resultset: ", data[i].id); repositories[repoName].resultSets.push(data[i]); added.push(data[i]); } else { @@ -502,7 +514,7 @@ treeherder.factory('ThResultSetModel', var appendResultSets = function(repoName, data) { if(data.length > 0){ - repositories[repoName].rsOffsetId = data[ data.length - 1 ].id; + Array.prototype.push.apply( repositories[repoName].resultSets, data @@ -556,7 +568,7 @@ treeherder.factory('ThResultSetModel', */ if(resultsetList.length > 0){ repositories[repoName].loadingStatus.prepending = true; - thResultSets.getResultSets(0, resultsetlist.length, resultsetlist). + thResultSets.getResultSets(0, resultsetList.length, resultsetList). success( _.bind(prependResultSets, $rootScope, repoName) ); } }; @@ -596,6 +608,8 @@ treeherder.factory('ThResultSetModel', fetchResultSets: fetchResultSets, + fetchJobs: fetchJobs, + aggregateJobPlatform: aggregateJobPlatform }; diff --git a/ui/js/services/models/user.js b/ui/js/models/user.js similarity index 100% rename from ui/js/services/models/user.js rename to ui/js/models/user.js diff --git a/ui/js/providers.js b/ui/js/providers.js index abed448a0..02379290f 100644 --- a/ui/js/providers.js +++ b/ui/js/providers.js @@ -10,42 +10,12 @@ treeherder.provider('thServiceDomain', function() { }; }); -treeherder.provider('thClassificationTypes', function() { - this.$get = function() { - return { - 1: { - name: "not classified", - star: "" - }, - 2: { - name: "expected fail", - star: "label-info" - }, - 3: { - name: "fixed by backout", - star: "label-success" - }, - 4: { - name: "intermittent", - star: "label-warning" - }, - 5: { - name: "infra", - star: "label-default" - }, - 6: { - name: "intermittent needs filing", - star: "label-danger" - } - }; - }; -}); - treeherder.provider('thResultStatusList', function() { this.$get = function() { return ['success', 'testfailed', 'busted', 'exception', 'retry', 'running', 'pending']; }; }); + treeherder.provider('thResultStatus', function() { this.$get = function() { return function(job) { @@ -57,6 +27,7 @@ treeherder.provider('thResultStatus', function() { }; }; }); + treeherder.provider('thResultStatusObject', function() { var getResultStatusObject = function(){ return { @@ -203,6 +174,9 @@ treeherder.provider('thEvents', function() { // fired (surprisingly) when a job is clicked jobClick: "job-click-EVT", + // fired when the job details are loaded + jobDetailLoaded: "job-detail-loaded-EVT", + // fired when a job is shift-clicked jobPin: "job-pin-EVT", @@ -230,9 +204,21 @@ treeherder.provider('thEvents', function() { toggleJobs: "toggle-jobs-EVT", + toggleUnclassifiedFailures: "toggle-unclassified-failures-EVT", + + selectNextUnclassifiedFailure: "next-unclassified-failure-EVT", + + selectPreviousUnclassifiedFailure: "previous-unclassified-failure-EVT", + searchPage: "search-page-EVT", - repoChanged: "repo-changed-EVT" + repoChanged: "repo-changed-EVT", + + // throwing this event will filter jobs to only show failures + // that have no classification. + showUnclassifiedFailures: "show-unclassified-failures-EVT", + + selectJob: "select-job-EVT" }; }; }); diff --git a/ui/js/services/classifications.js b/ui/js/services/classifications.js new file mode 100644 index 000000000..b7b02364f --- /dev/null +++ b/ui/js/services/classifications.js @@ -0,0 +1,35 @@ +'use strict'; + +treeherder.factory('thClassificationTypes', function($http, thUrl) { + + var classifications = {}; + + var classificationColors = { + 1: "", // not classified + 2: "label-info", // expected fail", + 3: "label-success", // fixed by backout", + 4: "label-warning", // intermittent", + 5: "label-default", // infra", + 6: "label-danger" // intermittent needs filing", + }; + + var addClassification = function(cl) { + classifications[cl.id] = { + name: cl.name, + star: classificationColors[cl.id] + }; + }; + + var load = function() { + return $http.get(thUrl.getRootUrl("/failureclassification/")). + success(function(data) { + _.forEach(data, addClassification); + }); + }; + + return { + classifications: classifications, + load: load + }; +}); + diff --git a/ui/js/services/jobfilters.js b/ui/js/services/jobfilters.js index cd69aba8d..f87d14151 100644 --- a/ui/js/services/jobfilters.js +++ b/ui/js/services/jobfilters.js @@ -21,7 +21,11 @@ * Each field is AND'ed so that, if a field exists in ``filters`` then the job * must match at least one value in every field. */ -treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope) { +treeherder.factory('thJobFilters', + function(thResultStatusList, ThLog, $rootScope, + ThResultSetModel, thPinboard, thNotify) { + + var $log = new ThLog("thJobFilters"); /** * If a custom resultStatusList is passed in (like for individual @@ -32,19 +36,29 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope * means it must have a value set, ``false`` means it must be null. */ var checkFilter = function(field, job, resultStatusList) { - // resultStatus is a special case that spans two job fields + $log.debug("checkFilter", field, job, resultStatusList); if (field === api.resultStatus) { + // resultStatus is a special case that spans two job fields var filterList = resultStatusList || filters[field].values; return _.contains(filterList, job.result) || _.contains(filterList, job.state); + } else if (field === api.failure_classification_id) { + // fci is a special case, too. Where 1 is "not classified" + var fci_filters = filters[field].values; + if (_.contains(fci_filters, false) && (job.failure_classification_id === 1 || + job.failure_classification_id === null)) { + return true; + } + return _.contains(fci_filters, true) && job.failure_classification_id > 1; } else { var jobFieldValue = getJobFieldValue(job, field); if (_.isUndefined(jobFieldValue)) { - //$log.warn("job object has no field of '" + field + "'. Skipping filtration."); + // if a filter is added somehow, but the job object doesn't + // have that field, then don't filter. Consider it a pass. return true; } - $log.debug(field + ": " + JSON.stringify(job)); + $log.debug("jobField filter", field, job); switch (filters[field].matchType) { case api.matchType.isnull: jobFieldValue = !_.isNull(jobFieldValue); @@ -117,7 +131,7 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope value = value.toLowerCase(); } if (filters.hasOwnProperty(field)) { - if (!_.contains(filters[field], value)) { + if (!_.contains(filters[field].values, value)) { filters[field].values.push(value); filters[field].matchType = matchType; } @@ -128,8 +142,11 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope removeWhenEmpty: true }; } - $log.debug("adding " + field + ": " + value); - $log.debug(filters); + + filterKeys = _.keys(filters); + + $log.debug("adding ", field, ": ", value); + $log.debug("filters", filters); }, removeFilter: function(field, value) { if (filters.hasOwnProperty(field)) { @@ -139,7 +156,7 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope } var idx = filters[field].values.indexOf(value); if(idx > -1) { - $log.debug("removing " + value); + $log.debug("removing ", value); filters[field].values.splice(idx, 1); } } @@ -149,7 +166,9 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope if (filters[field].removeWhenEmpty && filters[field].values.length === 0) { delete filters[field]; } - $log.debug(filters); + + filterKeys = _.keys(filters); + $log.debug("filters", filters); }, /** * used mostly for resultStatus doing group toggles @@ -159,7 +178,7 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope * @param add - true if adding, false if removing */ toggleFilters: function(field, values, add) { - $log.debug("toggling: " + add); + $log.debug("toggling: ", add); var action = add? api.addFilter: api.removeFilter; for (var i = 0; i < values.length; i++) { action(field, values[i]); @@ -182,7 +201,7 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope return false; } } - if($rootScope.searchQuery != ""){ + if(typeof $rootScope.searchQuery === 'string'){ //Confirm job matches search query if(job.searchableStr.toLowerCase().indexOf( $rootScope.searchQuery.toLowerCase() @@ -206,6 +225,31 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope getFilters: function() { return filters; }, + /** + * Pin all jobs that pass the GLOBAL filters. Ignores toggling at + * the result set level. + */ + pinAllShownJobs: function() { + var jobs = ThResultSetModel.getJobMap($rootScope.repoName); + var jobsToPin = []; + + var queuePinIfShown = function(jMap) { + if (api.showJob(jMap.job_obj)) { + jobsToPin.push(jMap.job_obj); + } + }; + _.forEach(jobs, queuePinIfShown); + + if (_.size(jobsToPin) > thPinboard.spaceRemaining()) { + jobsToPin = jobsToPin.splice(0, thPinboard.spaceRemaining()); + thNotify.send("Pinboard max size exceeded. Pinning only the first " + thPinboard.spaceRemaining(), + "danger", + true); + } + + $rootScope.selectedJob = jobsToPin[0]; + _.forEach(jobsToPin, thPinboard.pinJob); + }, // CONSTANTS failure_classification_id: "failure_classification_id", @@ -213,7 +257,8 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope matchType: { exactstr: 0, substr: 1, - isnull: 2 + isnull: 2, + bool: 3 } }; @@ -225,7 +270,7 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope removeWhenEmpty: false }, failure_classification_id: { - matchType: api.matchType.isnull, + matchType: api.matchType.bool, values: [true, false], removeWhenEmpty: false } diff --git a/ui/js/services/log.js b/ui/js/services/log.js new file mode 100644 index 000000000..453a7b325 --- /dev/null +++ b/ui/js/services/log.js @@ -0,0 +1,87 @@ +'use strict'; + +treeherder.factory('ThLog', function($log, ThLogConfig) { + // a logger that states the object doing the logging + + var ThLog = function(name) { + this.name = name; + }; + + /** + * If ``whitelist`` has values, then only show messages from those. + * If ``whitelist`` is empty, then skip any messages from ``blacklist`` items. + */ + var whitelist = ThLogConfig.whitelist; + var blacklist = ThLogConfig.blacklist; + + ThLog.prototype.getClassName = function() { + return this.name; + }; + + ThLog.prototype.debug = function() {logIt(this, $log.debug, arguments);}; + ThLog.prototype.log = function() {logIt(this, $log.log, arguments);}; + ThLog.prototype.warn = function() {logIt(this, $log.warn, arguments);}; + ThLog.prototype.info = function() {logIt(this, $log.info, arguments);}; + ThLog.prototype.error = function() {logIt(this, $log.error, arguments);}; + + var logIt = function(self, func, args) { + if ((whitelist.length && _.contains(whitelist, self.getClassName())) || + (blacklist.length && !_.contains(blacklist, self.getClassName())) || + (!whitelist.length && !blacklist.length)) { + var newArgs = Array.prototype.slice.call(args); + newArgs.unshift(self.getClassName()); + func.apply(null, newArgs); + } + }; + + return ThLog; +}); + + +/** + * You can use this to configure which debug lines you want to see in your + * ``local.conf.js`` file. You can see ONLY ``ResultSetCtrl`` lines by adding + * a line like: + * + * ThLogConfigProvider.setWhitelist([ + * 'ResultSetCtrl' + * ]); + * + * Note: even though this is called ThLogConfig, when you configure it, you must + * refer to it as a ``ThLogConfigProvider`` in ``local.conf.js``. + */ +treeherder.provider('ThLogConfig', function() { + this.whitelist = []; + this.blacklist = []; + + this.setBlacklist = function(bl) { + this.blacklist = bl; + }; + this.setWhitelist = function(wl) { + this.whitelist = wl; + }; + + this.$get = function() { + var self = this; + + return { + + whitelist: self.whitelist, + blacklist: self.blacklist + + }; + }; + +}); + +treeherder.config(["$provide", function($provide, ThLog) { + $provide.decorator("$log", ["$delegate", function($delegate) { + + $delegate.getInstance = function(className) { + return new ThLog(className); + }; + return $delegate; + + }]); + +}]); diff --git a/ui/js/services/main.js b/ui/js/services/main.js index eaae6e5af..c0e2eecb7 100755 --- a/ui/js/services/main.js +++ b/ui/js/services/main.js @@ -1,7 +1,7 @@ 'use strict'; /* Services */ -treeherder.factory('thUrl',['$rootScope', 'thServiceDomain', '$log', function($rootScope, thServiceDomain, $log) { +treeherder.factory('thUrl',['$rootScope', 'thServiceDomain', 'ThLog', function($rootScope, thServiceDomain, ThLog) { var thUrl = { getRootUrl: function(uri) { @@ -22,7 +22,9 @@ treeherder.factory('thUrl',['$rootScope', 'thServiceDomain', '$log', function($r }]); -treeherder.factory('thSocket', function ($rootScope, $log, thUrl) { +treeherder.factory('thSocket', function ($rootScope, ThLog, thUrl) { + var $log = new ThLog("thSocket"); + var socket = io.connect(thUrl.getSocketEventUrl()); socket.on('connect', function () { $log.debug('socketio connected'); @@ -107,7 +109,7 @@ treeherder.factory('ThPaginator', function(){ }); -treeherder.factory('BrowserId', function($http, $q, $log, thServiceDomain){ +treeherder.factory('BrowserId', function($http, $q, ThLog, thServiceDomain){ /* * BrowserId is a wrapper for the persona authentication service @@ -180,9 +182,11 @@ treeherder.factory('BrowserId', function($http, $q, $log, thServiceDomain){ return browserid; }); -treeherder.factory('thNotify', function($timeout, $log){ +treeherder.factory('thNotify', function($timeout, ThLog){ //a growl-like notification system + var $log = new ThLog("thNotify"); + var thNotify = { // message queue notifications: [], @@ -194,8 +198,7 @@ treeherder.factory('thNotify', function($timeout, $log){ * after a while or not */ send: function(message, severity, sticky){ - $log.debug("received message"); - $log.debug(message); + $log.debug("received message", message); var severity = severity || 'info'; var sticky = sticky || false; thNotify.notifications.push({ diff --git a/ui/js/services/models/repository.js b/ui/js/services/models/repository.js deleted file mode 100644 index ff9b061a5..000000000 --- a/ui/js/services/models/repository.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict'; - -treeherder.factory('ThRepositoryModel', - ['$http', 'thUrl', '$rootScope', '$log', 'localStorageService', 'thSocket', 'thEvents', - function($http, thUrl, $rootScope, $log, localStorageService, thSocket, thEvents) { - - var new_failures = {}; - - thSocket.on('job_failure', function(msg){ - if (! new_failures.hasOwnProperty(msg.branch)){ - new_failures[msg.branch] = []; - } - new_failures[msg.branch].push(msg.id); - $log.debug("new failure on branch "+msg.branch); - }); - - - // get the repositories (aka trees) - // sample: 'resources/menu.json' - var byName = function(name) { - if ($rootScope.repos !== undefined) { - for (var i = 0; i < $rootScope.repos.length; i++) { - var repo = $rootScope.repos[i]; - if (repo.name === name) { - return repo; - } - } - } else { - $log.warn("Repos list has not been loaded."); - } - $log.warn("'" + name + "' not found in repos list."); - return null; - }; - - - // get by category - var byGroup = function() { - var groupedRepos = {}; - var group = function(repo) { - if (!_.has(groupedRepos, repo.repository_group.name)) { - groupedRepos[repo.repository_group.name] = []; - } - groupedRepos[repo.repository_group.name].push(repo); - }; - - if (!groupedRepos.length) { - _.each($rootScope.repos, group); - } - return groupedRepos; - }; - - var addAsUnwatched = function(repo) { - api.watchedRepos[repo.name] = false; - }; - - var api = { - // load the list of repos into $rootScope, and set the current repo. - load: function(name) { - - var storedWatchedRepos = localStorageService.get("watchedRepos") || {}; - $log.debug("stored watchedRepos"); - $log.debug(storedWatchedRepos); - - return api.get_list(). - success(function(data) { - $rootScope.repos = data; - $rootScope.groupedRepos = byGroup(); - _.each(data, addAsUnwatched); - _.extend(api.watchedRepos, storedWatchedRepos); - - if (name) { - $rootScope.currentRepo = byName(name); - - } - }); - }, - get_uri : function(){return thUrl.getRootUrl("/repository/");}, - - get_list: function(){ - return $http.get(api.get_uri(), {cache: true}) - }, - - // return the currently selected repo - getCurrent: function() { - return $rootScope.currentRepo; - }, - // set the current repo to one in the repos list - setCurrent: function(name) { - $rootScope.currentRepo = byName(name); - api.watchedRepos[name] = true; - api.saveWatchedRepos(); - }, - // get a repo object without setting anything - getRepo: function(name) { - return byName(name); - }, - getByGroup: function() { - return byGroup(); - }, - watchedRepos: {}, - saveWatchedRepos: function() { - localStorageService.set("watchedRepos", api.watchedRepos); - - $log.debug("saveWatchedRepos"); - $log.debug(localStorageService.get("watchedRepos")); - }, - repo_has_failures: function(repo_name){ - return ($rootScope.new_failures.hasOwnProperty(repo_name) && - $rootScope.new_failures[repo_name].length > 0); - } - - }; - - return api; -}]); diff --git a/ui/js/services/pinboard.js b/ui/js/services/pinboard.js index e72dbe99e..a2cf5ad6a 100644 --- a/ui/js/services/pinboard.js +++ b/ui/js/services/pinboard.js @@ -2,7 +2,9 @@ treeherder.factory('thPinboard', function($http, thUrl, ThJobClassificationModel, $rootScope, - thEvents, ThBugJobMapModel, thNotify) { + thEvents, ThBugJobMapModel, thNotify, ThLog) { + + var $log = new ThLog("thPinboard"); var pinnedJobs = {}; var relatedBugs = {}; @@ -10,15 +12,18 @@ treeherder.factory('thPinboard', var saveClassification = function(job) { var classification = new ThJobClassificationModel(this); - job.failure_classification_id = classification.failure_classification_id; + // classification can be left unset making this a no-op + if (classification.failure_classification_id > 0) { + job.failure_classification_id = classification.failure_classification_id; - classification.job_id = job.id; - classification.create(). - success(function(data) { - thNotify.send("classification saved for " + job.platform + ": " + job.job_type_name, "success"); - }).error(function(data) { - thNotify.send("error saving classification for " + job.platform + ": " + job.job_type_name, "danger"); - }); + classification.job_id = job.id; + classification.create(). + success(function(data) { + thNotify.send("classification saved for " + job.platform + ": " + job.job_type_name, "success"); + }).error(function(data) { + thNotify.send("error saving classification for " + job.platform + ": " + job.job_type_name, "danger"); + }); + } }; var saveBugs = function(job) { @@ -40,8 +45,12 @@ treeherder.factory('thPinboard', var api = { pinJob: function(job) { - pinnedJobs[job.id] = job; - api.count.numPinnedJobs = _.size(pinnedJobs); + if (api.spaceRemaining() > 0) { + pinnedJobs[job.id] = job; + api.count.numPinnedJobs = _.size(pinnedJobs); + } else { + thNotify.send("Pinboard is already at maximum size of " + api.maxNumPinned, "danger", true); + } }, unPinJob: function(id) { @@ -59,8 +68,11 @@ treeherder.factory('thPinboard', }, addBug: function(bug) { + $log.debug("adding bug ", bug); relatedBugs[bug.id] = bug; api.count.numRelatedBugs = _.size(relatedBugs); + $log.debug("related bugs", relatedBugs); + }, removeBug: function(id) { @@ -105,23 +117,26 @@ treeherder.factory('thPinboard', // save bug associations only on all pinned jobs saveBugsOnly: function() { - if (!_.size(relatedBugs)) { - thNotify.send("no bug associations to save"); - } else { - _.each(pinnedJobs, saveBugs); - $rootScope.$broadcast(thEvents.bugsAssociated, {jobs: pinnedJobs}); - } + _.each(pinnedJobs, saveBugs); + $rootScope.$broadcast(thEvents.bugsAssociated, {jobs: pinnedJobs}); }, hasPinnedJobs: function() { return !_.isEmpty(pinnedJobs); }, + + spaceRemaining: function() { + return api.maxNumPinned - api.count.numPinnedJobs; + }, + pinnedJobs: pinnedJobs, relatedBugs: relatedBugs, count: { numPinnedJobs: 0, numRelatedBugs: 0 - } + }, + // not sure what this should be, but we need some limit, I think. + maxNumPinned: 500 }; return api; diff --git a/ui/js/services/resultsets.js b/ui/js/services/resultsets.js index 82addfb5d..725197042 100644 --- a/ui/js/services/resultsets.js +++ b/ui/js/services/resultsets.js @@ -1,7 +1,6 @@ 'use strict'; treeherder.factory('thResultSets', - ['$http', '$location', 'thUrl', 'thServiceDomain', function($http, $location, thUrl, thServiceDomain) { // get the resultsets for this repo @@ -42,4 +41,4 @@ treeherder.factory('thResultSets', return $http.get(thServiceDomain + uri, {params: {format: "json"}}); } }; -}]); +}); diff --git a/ui/js/services/treestatus.js b/ui/js/services/treestatus.js new file mode 100644 index 000000000..7cc217609 --- /dev/null +++ b/ui/js/services/treestatus.js @@ -0,0 +1,31 @@ +'use strict'; + +treeherder.factory('treeStatus', function($http, $q) { + + var urlBase = "https://treestatus.mozilla.org/"; + + var getTreeStatusName = function(name) { + // the thunderbird names in treestatus.mozilla.org don't match what + // we use, so translate them. pretty hacky, yes... + if (name.contains("thunderbird")) { + if (name === "thunderbird-trunk") { + return "comm-central-thunderbird"; + } else { + var tokens = name.split("-"); + return "comm-" + tokens[1] + "-" + tokens[0]; + } + } + return name; + }; + + var get = function(repoName) { + var url = urlBase + getTreeStatusName(repoName); + + return $http.get(url, {params: {format: "json"}}); + }; + + return { + get: get + }; +}); + diff --git a/ui/logviewer.html b/ui/logviewer.html index 4f8ab9d0f..1623869cd 100644 --- a/ui/logviewer.html +++ b/ui/logviewer.html @@ -9,39 +9,37 @@
-
- -
{{label}}
-
{{value}}
-
-
-
-
- {{step.order+1}}. {{step.name}} - {{time}} -
-

-

- {{error.linenumber}} - {{error.line}} -
-

-
+ + + + +
{{label}}{{value}}
+

Select one of these steps to see more details:

+
+ {{step.order+1}}. {{step.name}} + {{time}} +
+

+

+ {{error.linenumber}} + {{error.line | limitTo: 67}}... +
+

-
+
- + + diff --git a/ui/partials/thActionButton.html b/ui/partials/thActionButton.html index 0527ac016..4be1b1706 100644 --- a/ui/partials/thActionButton.html +++ b/ui/partials/thActionButton.html @@ -6,7 +6,7 @@ diff --git a/ui/partials/thStatusFilterPanel.html b/ui/partials/thFilterPanel.html similarity index 87% rename from ui/partials/thStatusFilterPanel.html rename to ui/partials/thFilterPanel.html index 5c92fb4fd..6810ee3e6 100644 --- a/ui/partials/thStatusFilterPanel.html +++ b/ui/partials/thFilterPanel.html @@ -1,7 +1,16 @@
- + ng-controller="FilterPanelCtrl"> + - repos - - - - filters - - - - help - {{user.email}} - - - Settings - - + + Repos + + + + Filters + + + + Help + {{user.email}} + + Settings + + + + - - +
+ +
- -
- -
- - - + + + diff --git a/ui/partials/thJobButton.html b/ui/partials/thJobButton.html deleted file mode 100644 index 9b8342a6c..000000000 --- a/ui/partials/thJobButton.html +++ /dev/null @@ -1,9 +0,0 @@ - - {{ job.job_type_symbol }} - diff --git a/ui/partials/thPinboardPanel.html b/ui/partials/thPinboardPanel.html index 2e901540c..57df29440 100644 --- a/ui/partials/thPinboardPanel.html +++ b/ui/partials/thPinboardPanel.html @@ -1,4 +1,5 @@
pinboard
@@ -9,6 +10,7 @@
diff --git a/ui/partials/thRelatedBug.html b/ui/partials/thRelatedBugQueued.html similarity index 100% rename from ui/partials/thRelatedBug.html rename to ui/partials/thRelatedBugQueued.html diff --git a/ui/partials/thRelatedBugSaved.html b/ui/partials/thRelatedBugSaved.html new file mode 100644 index 000000000..5b7bb249c --- /dev/null +++ b/ui/partials/thRelatedBugSaved.html @@ -0,0 +1,9 @@ + + {{bug.bug_id}} + + diff --git a/ui/partials/thRepoDropDown.html b/ui/partials/thRepoDropDown.html new file mode 100644 index 000000000..40ee2e4e0 --- /dev/null +++ b/ui/partials/thRepoDropDown.html @@ -0,0 +1,38 @@ + diff --git a/ui/partials/thRepoPanel.html b/ui/partials/thRepoPanel.html index 6a91c2b09..8e9056082 100644 --- a/ui/partials/thRepoPanel.html +++ b/ui/partials/thRepoPanel.html @@ -13,12 +13,19 @@
- + {{repo.name}} + + + +
diff --git a/ui/partials/thWatchedRepo.html b/ui/partials/thWatchedRepo.html new file mode 100644 index 000000000..d99136bdf --- /dev/null +++ b/ui/partials/thWatchedRepo.html @@ -0,0 +1,27 @@ + + + + + + + diff --git a/ui/partials/thWatchedRepoPanel.html b/ui/partials/thWatchedRepoPanel.html index 18d92c5c2..f601b05fc 100644 --- a/ui/partials/thWatchedRepoPanel.html +++ b/ui/partials/thWatchedRepoPanel.html @@ -1,17 +1,13 @@ -
- {{repo}} {{failures[repo]}} - +
+ +