This commit is contained in:
mdoglio 2014-04-10 12:25:09 +01:00
Родитель 3e625f31b7 2f7bc79c70
Коммит 6b0c56eb29
60 изменённых файлов: 2032 добавлений и 1213 удалений

Просмотреть файл

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

Просмотреть файл

@ -54,12 +54,8 @@
<div class="panel-heading"><h3>Keyboard shortcuts</h3></div> <div class="panel-heading"><h3>Keyboard shortcuts</h3></div>
<div class="panel-body"> <div class="panel-body">
<table id="shortcuts"> <table id="shortcuts">
<tr><th>space</th> <tr><th>s</th>
<td>Select/deselect active build or changeset</td></tr> <td>Add selected job to the pin board</td></tr>
<tr><th>?</th>
<td>Show the help box</td></tr>
<tr><th>c</th>
<td>Show the comment box</td></tr>
<tr><th>j</th> <tr><th>j</th>
<td>Highlight next unstarred failure</td></tr> <td>Highlight next unstarred failure</td></tr>
<tr><th>k</th> <tr><th>k</th>
@ -69,15 +65,9 @@
<tr><th>p</th> <tr><th>p</th>
<td>Highlight previous unstarred failure</td></tr> <td>Highlight previous unstarred failure</td></tr>
<tr><th>u</th> <tr><th>u</th>
<td>Toggle showing only unstarred failures</td></tr> <td>Show only unstarred failures</td></tr>
<tr><th>Click</th> <tr><th>Shift-Click</th>
<td>Choose an active build and display its details</td></tr> <td>Add job to the pinboard</td></tr>
<tr><th>Ctrl/Cmd-Click</th>
<td>Select/deselect build or changeset</td></tr>
<tr><th>Drag</th>
<td>Add build or changeset to comment</td></tr>
<tr><th>Ctrl/Cmd-Enter</th>
<td>Submit the comment form</td></tr>
</table> </table>
</div> </div>
</div> </div>
@ -317,4 +307,4 @@
</div> </div>
</div> </div>
</body> </body>
</html> </html>

Просмотреть файл

@ -13,16 +13,11 @@
<link href="css/persona-buttons.css" rel="stylesheet" media="screen"> <link href="css/persona-buttons.css" rel="stylesheet" media="screen">
</head> </head>
<body ng-controller="MainCtrl"> <body ng-controller="MainCtrl" ng-keydown="processKeyboardInput($event)">
<th-global-top-nav-panel></th-global-top-nav-panel> <ng-include id="th-global-top-nav-panel" src="'partials/thGlobalTopNavPanel.html'"></ng-include>
<div class="th-content"> <div class="th-content">
<span class="th-view-content" ng-cloak> <span class="th-view-content" ng-cloak>
<div class="alert"
ng-bind="statusMsg"
ng-show="statusMsg"
ng-class="{'alert-success': (statusColor=='green'), 'alert-error': (statusColor=='red')}">
</div>
<ng-view ></ng-view> <ng-view ></ng-view>
</span> </span>
@ -31,7 +26,7 @@
<!-- Footer --> <!-- Footer -->
<div class="nav navbar navbar-default navbar-fixed-bottom bottom-panel" ng-show="selectedJob"> <div class="nav navbar navbar-default navbar-fixed-bottom bottom-panel" ng-show="selectedJob">
<resizable-panel></resizable-panel> <resizable-panel></resizable-panel>
<div ng-include src="'plugins/pluginpanel.html'"></div> <div class="full-height" ng-include src="'plugins/pluginpanel.html'"></div>
</div> </div>
<th-notification-box></th-notification-box> <th-notification-box></th-notification-box>
@ -47,26 +42,39 @@
<script src="vendor/socket.io.js"></script> <script src="vendor/socket.io.js"></script>
<script src="vendor/angular-local-storage.min.js"></script> <script src="vendor/angular-local-storage.min.js"></script>
<script src="vendor/underscore-min.js"></script> <script src="vendor/underscore-min.js"></script>
<script src="js/config/local.conf.js"></script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
<script src="js/services/log.js"></script>
<script src="js/config/local.conf.js"></script>
<script src="js/providers.js"></script> <script src="js/providers.js"></script>
<script src="js/directives.js"></script> <!-- Directives -->
<script src="js/directives/main.js"></script>
<script src="js/directives/clonejobs.js"></script>
<script src="js/directives/persona.js"></script>
<script src="js/directives/resultsets.js"></script>
<script src="js/directives/top_nav_bar.js"></script>
<script src="js/directives/bottom_nav_panel.js"></script>
<!-- Main services -->
<script src="js/services/main.js"></script> <script src="js/services/main.js"></script>
<script src="js/services/jobfilters.js"></script> <script src="js/services/jobfilters.js"></script>
<script src="js/services/classifications.js"></script>
<script src="js/services/pinboard.js"></script> <script src="js/services/pinboard.js"></script>
<script src="js/services/resultsets.js"></script> <script src="js/services/resultsets.js"></script>
<script src="js/services/models/resultsets.js"></script> <script src="js/services/treestatus.js"></script>
<script src="js/services/models/job_artifact.js"></script> <!-- Model services -->
<script src="js/services/models/job_filter.js"></script> <script src="js/models/resultsets.js"></script>
<script src="js/services/models/exclusion_profile.js"></script> <script src="js/models/job_artifact.js"></script>
<script src="js/services/models/repository.js"></script> <script src="js/models/repository.js"></script>
<script src="js/services/models/bug_job_map.js"></script> <script src="js/models/bug_job_map.js"></script>
<script src="js/services/models/build_platform.js"></script> <script src="js/models/classification.js"></script>
<script src="js/services/models/job_type.js"></script> <script src="js/models/job.js"></script>
<script src="js/services/models/classification.js"></script> <script src="js/models/job_filter.js"></script>
<script src="js/services/models/option.js"></script> <script src="js/models/exclusion_profile.js"></script>
<script src="js/services/models/job.js"></script> <script src="js/models/build_platform.js"></script>
<script src="js/services/models/user.js"></script> <script src="js/models/job_type.js"></script>
<script src="js/models/option.js"></script>
<script src="js/models/user.js"></script>
<!-- Controllers -->
<script src="js/controllers/main.js"></script> <script src="js/controllers/main.js"></script>
<script src="js/controllers/sheriff.js"></script> <script src="js/controllers/sheriff.js"></script>
<script src="js/controllers/settings.js"></script> <script src="js/controllers/settings.js"></script>
@ -75,12 +83,14 @@
<script src="js/controllers/jobs.js"></script> <script src="js/controllers/jobs.js"></script>
<script src="js/controllers/machines.js"></script> <script src="js/controllers/machines.js"></script>
<script src="js/controllers/timeline.js"></script> <script src="js/controllers/timeline.js"></script>
<!-- Plugins -->
<script src="plugins/controller.js"></script> <script src="plugins/controller.js"></script>
<script src="plugins/pinboard.js"></script> <script src="plugins/pinboard.js"></script>
<script src="plugins/annotations/controller.js"></script> <script src="plugins/annotations/controller.js"></script>
<script src="plugins/tinderbox/controller.js"></script> <script src="plugins/tinderbox/controller.js"></script>
<script src="plugins/bugs_suggestions/controller.js"></script> <script src="plugins/bugs_suggestions/controller.js"></script>
<script src="plugins/similar_jobs/controller.js"></script> <script src="plugins/similar_jobs/controller.js"></script>
<script src="js/filters.js"></script> <script src="js/filters.js"></script>
<script src="vendor/Config.js"></script> <script src="vendor/Config.js"></script>
<script src="https://login.persona.org/include.js"></script> <script src="https://login.persona.org/include.js"></script>
@ -147,9 +157,28 @@
<!-- Job Btn span --> <!-- Job Btn span -->
<script type="'text/ng-template'" id="jobBtnClone.html"> <script type="'text/ng-template'" id="jobBtnClone.html">
<span style="margin-right:1px;" class="btn job-btn btn-xs {{ btnClass }}" data-jmkey="{{ key }}" title="{{ title }}">{{ value }}</span> <span style="margin-right:1px;" class="btn job-btn btn-xs {{ btnClass }} {{ key }}" data-jmkey="{{ key }}" title="{{ title }}">{{ value }}</span>
</script> </script>
<!-- Tooltip for job info-->
<script type="'text/ng-template'" id="jobInfoTooltip.html">
<div>
<table class="table-super-condensed table-striped">
<tr>
<th class="small">Result</th>
<td class="small {{ resultStatusClass }}">{{ job.result }}</td>
</tr>
<tr>
<th class="small">Machine name</th>
<td class="small">
<a target="_blank" href="https://secure.pub.build.mozilla.org/builddata/reports/slave_health/slave.html?name={{ job.machine_name }}">{{ job.machine_name }}</a>
</td>
</tr>
<tr ng-repeat="(label, value) in visibleFields"><th>{{label}}</th><td>{{ value }}</td></tr>
</table>
</div>
</script>
</body> </body>
</html> </html>

Просмотреть файл

@ -43,18 +43,4 @@ treeherder.config(function($routeProvider, $httpProvider, $logProvider) {
otherwise({redirectTo: '/jobs'}); otherwise({redirectTo: '/jobs'});
}); });
var logViewer = angular.module('logViewer',['treeherder']); 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';
});

Просмотреть файл

@ -1,6 +1,34 @@
'use strict';
// mozilla hosted service // mozilla hosted service
//window.thServiceDomain = "http://dev.treeherder.mozilla.org"; //window.thServiceDomain = "http://dev.treeherder.mozilla.org";
// local vagrant instance of service // local vagrant instance of service
window.thServiceDomain = "http://local.treeherder.mozilla.org"; 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',
// ...
]);
}]);

Просмотреть файл

@ -1,8 +1,10 @@
"use strict"; "use strict";
treeherder.controller('StatusFilterPanelCtrl', treeherder.controller('FilterPanelCtrl',
function StatusFilterPanelCtrl($scope, $rootScope, $routeParams, $location, $log, function FilterPanelCtrl($scope, $rootScope, $routeParams, $location, ThLog,
localStorageService, thResultStatusList, thEvents, thJobFilters) { localStorageService, thResultStatusList, thEvents,
thJobFilters) {
var $log = new ThLog(this.constructor.name);
$scope.filterOptions = thResultStatusList; $scope.filterOptions = thResultStatusList;
@ -29,8 +31,10 @@ treeherder.controller('StatusFilterPanelCtrl',
/** /**
* Handle checking the "all" button for a result status group * 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) { var check = function(rs) {
$scope.resultStatusFilters[rs] = group.allChecked; $scope.resultStatusFilters[rs] = group.allChecked;
}; };
@ -41,9 +45,35 @@ treeherder.controller('StatusFilterPanelCtrl',
group.resultStatuses, group.resultStatuses,
group.allChecked group.allChecked
); );
$rootScope.$broadcast(thEvents.globalFilterChanged,
{target: group, newValue: group.allChecked}); if (!quiet) {
showCheck(); $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, $rootScope.$broadcast(thEvents.globalFilterChanged,
{target: filter, newValue: $scope.resultStatusFilters[filter]}); {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`` * ``classified`` (when true) or ``unclassified``
* (when false) * (when false)
*/ */
$scope.toggleClassificationFilter = function(isClassified) { $scope.setClassificationFilter = function(isClassified, isChecked, quiet) {
var field = "failure_classification_id"; var field = "failure_classification_id";
// this function is called before the checkbox value has actually // this function is called before the checkbox value has actually
// changed the scope model value, so change to the inverse. // 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 func = isChecked? thJobFilters.addFilter: thJobFilters.removeFilter;
var target = isClassified? "classified": "unclassified"; var target = isClassified? "classified": "unclassified";
func(field, isClassified, thJobFilters.matchType.isnull); func(field, isClassified, thJobFilters.matchType.bool);
$rootScope.$broadcast(thEvents.globalFilterChanged, if (!quiet) {
{target: target, newValue: isChecked}); $rootScope.$broadcast(thEvents.globalFilterChanged,
showCheck(); {target: target, newValue: isChecked});
}
}; };
$scope.createFieldFilter = function() { $scope.createFieldFilter = function() {
@ -96,7 +134,7 @@ treeherder.controller('StatusFilterPanelCtrl',
$scope.addFieldFilter = function() { $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 === "") { if (!$scope.newFieldFilter || $scope.newFieldFilter.field === "" || $scope.newFieldFilter.value === "") {
return; return;
} }
@ -112,7 +150,6 @@ treeherder.controller('StatusFilterPanelCtrl',
$rootScope.$broadcast(thEvents.globalFilterChanged, $rootScope.$broadcast(thEvents.globalFilterChanged,
{target: $scope.newFieldFilter.field, newValue: $scope.newFieldFilter.value}); {target: $scope.newFieldFilter.field, newValue: $scope.newFieldFilter.value});
$scope.newFieldFilter = null; $scope.newFieldFilter = null;
showCheck();
}; };
@ -123,11 +160,10 @@ treeherder.controller('StatusFilterPanelCtrl',
$rootScope.$broadcast(thEvents.globalFilterChanged, $rootScope.$broadcast(thEvents.globalFilterChanged,
{target: "allFieldFilters", newValue: null}); {target: "allFieldFilters", newValue: null});
$scope.fieldFilters = []; $scope.fieldFilters = [];
showCheck();
}; };
$scope.removeFilter = function(index) { $scope.removeFilter = function(index) {
$log.debug("removing index: " + index); $log.debug("removing index", index);
thJobFilters.removeFilter( thJobFilters.removeFilter(
$scope.fieldFilters[index].field, $scope.fieldFilters[index].field,
$scope.fieldFilters[index].value $scope.fieldFilters[index].value
@ -135,40 +171,11 @@ treeherder.controller('StatusFilterPanelCtrl',
$rootScope.$broadcast(thEvents.globalFilterChanged, $rootScope.$broadcast(thEvents.globalFilterChanged,
{target: $scope.fieldFilters[index].field, newValue: null}); {target: $scope.fieldFilters[index].field, newValue: null});
$scope.fieldFilters.splice(index, 1); $scope.fieldFilters.splice(index, 1);
showCheck();
}; };
/* $scope.pinAllShownJobs = function() {
@@@ TODO: CAMD: test code, remove before merge. thJobFilters.pinAllShownJobs();
*/
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()));
}; };
// END test code
$scope.resultStatusFilters = {}; $scope.resultStatusFilters = {};
for (var i = 0; i < $scope.filterOptions.length; i++) { for (var i = 0; i < $scope.filterOptions.length; i++) {

Просмотреть файл

@ -1,9 +1,10 @@
"use strict"; "use strict";
treeherder.controller('JobsCtrl', treeherder.controller('JobsCtrl',
function JobsCtrl($scope, $http, $rootScope, $routeParams, $log, $cookies, function JobsCtrl($scope, $http, $rootScope, $routeParams, ThLog, $cookies,
localStorageService, thUrl, ThRepositoryModel, thSocket, localStorageService, thUrl, ThRepositoryModel, thSocket,
ThResultSetModel, thResultStatusList) { ThResultSetModel, thResultStatusList) {
var $log = new ThLog(this.constructor.name);
// load our initial set of resultsets // load our initial set of resultsets
// scope needs this function so it can be called directly by the user, too. // scope needs this function so it can be called directly by the user, too.
@ -18,8 +19,9 @@ treeherder.controller('JobsCtrl',
} else { } else {
$rootScope.repoName = "mozilla-inbound"; $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); ThResultSetModel.addRepository($scope.repoName);
@ -28,9 +30,6 @@ treeherder.controller('JobsCtrl',
$scope.job_map = ThResultSetModel.getJobMap($scope.repoName); $scope.job_map = ThResultSetModel.getJobMap($scope.repoName);
$scope.statusList = thResultStatusList; $scope.statusList = thResultStatusList;
// load the list of repos into $rootScope, and set the current repo.
ThRepositoryModel.load($scope.repoName);
if(ThResultSetModel.isNotLoaded($scope.repoName)){ if(ThResultSetModel.isNotLoaded($scope.repoName)){
// get our first set of resultsets // get our first set of resultsets
$scope.fetchResultSets(10); $scope.fetchResultSets(10);
@ -41,9 +40,11 @@ treeherder.controller('JobsCtrl',
treeherder.controller('ResultSetCtrl', treeherder.controller('ResultSetCtrl',
function ResultSetCtrl($scope, $rootScope, $http, $log, $location, function ResultSetCtrl($scope, $rootScope, $http, ThLog, $location,
thUrl, thServiceDomain, thResultStatusInfo, thUrl, thServiceDomain, thResultStatusInfo,
ThResultSetModel, thEvents, thJobFilters, $route) { ThResultSetModel, thEvents, thJobFilters) {
var $log = new ThLog(this.constructor.name);
$scope.getCountClass = function(resultStatus) { $scope.getCountClass = function(resultStatus) {
return thResultStatusInfo(resultStatus).btnClass; return thResultStatusInfo(resultStatus).btnClass;
@ -111,8 +112,8 @@ treeherder.controller('ResultSetCtrl',
thEvents.resultSetFilterChanged, $scope.resultset thEvents.resultSetFilterChanged, $scope.resultset
); );
$log.debug("toggled: " + resultStatus); $log.debug("toggled: ", resultStatus);
$log.debug($scope.resultStatusFilters); $log.debug("resultStatusFilters", $scope.resultStatusFilters);
}; };
/** /**
@ -125,7 +126,7 @@ treeherder.controller('ResultSetCtrl',
}; };
$scope.revisionResultsetFilterUrl = $scope.urlBasePath + "?repo=" + $scope.repoName + "&revision=" + $scope.resultset.revision; $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(); $scope.resultStatusFilters = thJobFilters.copyResultStatusFilters();
@ -135,7 +136,7 @@ treeherder.controller('ResultSetCtrl',
$scope.isCollapsedRevisions = true; $scope.isCollapsedRevisions = true;
$rootScope.$on(thEvents.jobContextMenu, function(event, job){ $rootScope.$on(thEvents.jobContextMenu, function(event, job){
$log.debug(thEvents.jobContextMenu + ' caught'); $log.debug("caught", thEvents.jobContextMenu);
//$scope.viewLog(job.resource_uri); //$scope.viewLog(job.resource_uri);
}); });
} }

Просмотреть файл

@ -1,7 +1,9 @@
'use strict'; 'use strict';
logViewer.controller('LogviewerCtrl', 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(); var query_string = $location.search();
if (query_string.repo !== "") { if (query_string.repo !== "") {

Просмотреть файл

@ -1,22 +1,49 @@
"use strict"; "use strict";
treeherder.controller('MainCtrl', treeherder.controller('MainCtrl',
function MainController($scope, $rootScope, $routeParams, $location, $log, function MainController($scope, $rootScope, $routeParams, $location, ThLog,
localStorageService, ThRepositoryModel, thPinboard, localStorageService, ThRepositoryModel, thPinboard,
ThExclusionProfileModel, thEvents) { thClassificationTypes, thEvents, $interval, ThExclusionProfileModel) {
$scope.query="";
$scope.statusError = function(msg) { var $log = new ThLog("MainCtrl");
$rootScope.statusMsg = msg;
$rootScope.statusColor = "red"; thClassificationTypes.load();
}; ThRepositoryModel.load();
$scope.statusSuccess = function(msg) {
$rootScope.statusMsg = msg;
$rootScope.statusColor = "green";
};
$scope.clearJob = function() { $scope.clearJob = function() {
// setting the selectedJob to null hides the bottom panel // setting the selectedJob to null hides the bottom panel
$rootScope.selectedJob = null; $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 // detect window width and put it in scope so items can react to
// a narrow/wide window // a narrow/wide window
@ -30,6 +57,45 @@ treeherder.controller('MainCtrl',
$scope.$apply(); $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 // give the page a way to determine which nav toolbar to show
$rootScope.$on('$locationChangeSuccess', function(ev,newUrl) { $rootScope.$on('$locationChangeSuccess', function(ev,newUrl) {
$rootScope.locationPath = $location.path().replace('/', ''); $rootScope.locationPath = $location.path().replace('/', '');
@ -37,9 +103,6 @@ treeherder.controller('MainCtrl',
$rootScope.urlBasePath = $location.absUrl().split('?')[0]; $rootScope.urlBasePath = $location.absUrl().split('?')[0];
// the repos the user has chosen to watch
$scope.watchedRepos = ThRepositoryModel.watchedRepos;
$scope.changeRepo = function(repo_name) { $scope.changeRepo = function(repo_name) {
// hide the repo panel if they chose to load one. // hide the repo panel if they chose to load one.
$scope.isRepoPanelShowing = false; $scope.isRepoPanelShowing = false;

Просмотреть файл

@ -1,12 +1,9 @@
"use strict"; "use strict";
treeherder.controller('RepositoryPanelCtrl', treeherder.controller('RepositoryPanelCtrl',
function RepositoryPanelCtrl($scope, $rootScope, $routeParams, $location, $log, function RepositoryPanelCtrl($scope, $rootScope, $routeParams, $location, ThLog,
localStorageService, ThRepositoryModel, thSocket) { localStorageService, ThRepositoryModel, thSocket) {
var $log = new ThLog(this.constructor.name);
$scope.saveWatchedRepos = function() {
ThRepositoryModel.saveWatchedRepos();
};
for (var repo in $scope.watchedRepos) { for (var repo in $scope.watchedRepos) {
if($scope.watchedRepos[repo]){ if($scope.watchedRepos[repo]){
@ -14,6 +11,9 @@ treeherder.controller('RepositoryPanelCtrl',
$log.debug("subscribing to "+repo+".job_failure"); $log.debug("subscribing to "+repo+".job_failure");
} }
} }
$scope.toggleRepo = function(repoName) {
$scope.watchedRepos[repoName].isWatched = !$scope.watchedRepos[repoName].isWatched;
ThRepositoryModel.watchedReposUpdated(repoName);
};
} }
); );

Просмотреть файл

@ -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: '<span class="label {{ badgeColorClass}}" ' +
'title="{{ hoverText }}">' +
'<i class="glyphicon glyphicon-star-empty"></i>' +
'</span> {{ 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"
};
});

802
ui/js/directives.js → ui/js/directives/clonejobs.js Executable file → Normal file
Просмотреть файл

@ -2,11 +2,19 @@
/* Directives */ /* Directives */
treeherder.directive('thCloneJobs', function( treeherder.directive('thCloneJobs', function(
$rootScope, $http, $log, thUrl, thCloneHtml, thServiceDomain, $rootScope, $http, ThLog, thUrl, thCloneHtml, thServiceDomain,
thResultStatusInfo, thEvents, thAggregateIds, thJobFilters, thResultStatusInfo, thEvents, thAggregateIds, thJobFilters,
thResultStatusObject, ThResultSetModel){ thResultStatusObject, ThResultSetModel){
var lastJobElSelected = {}; var $log = new ThLog("thCloneJobs");
var lastJobElSelected, lastJobObjSelected;
var classificationRequired = {
"busted":1,
"exception":1,
"testfailed":1
};
// CSS classes // CSS classes
var btnCls = 'btn-xs'; var btnCls = 'btn-xs';
@ -38,15 +46,73 @@ treeherder.directive('thCloneJobs', function(
}; };
var getHoverText = function(job) { var getHoverText = function(job) {
var duration = Math.round((job.end_timestamp - job.submit_timestamp) / 60);
var jobStatus = job.result; var jobStatus = job.result;
if (job.state != "completed") { if (job.state !== "completed") {
jobStatus = job.state; 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)){ if(!_.isEmpty(lastJobElSelected)){
lastJobElSelected.removeClass(selectedBtnCls); lastJobElSelected.removeClass(selectedBtnCls);
@ -61,7 +127,7 @@ treeherder.directive('thCloneJobs', function(
}; };
var clickJobCb = function(ev, el, job){ var clickJobCb = function(ev, el, job){
selectJob(el); setSelectJobStyles(el);
$rootScope.$broadcast(thEvents.jobClick, job); $rootScope.$broadcast(thEvents.jobClick, job);
}; };
@ -82,7 +148,7 @@ treeherder.directive('thCloneJobs', function(
} }
}); });
} else { } 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 //Set the resultState
resultState = job.result; resultState = job.result;
if (job.state != "completed") { if (job.state !== "completed") {
resultState = job.state; resultState = job.state;
} }
resultState = resultState || 'unknown'; resultState = resultState || 'unknown';
if(job.job_coalesced_to_guid != null){ if(job.job_coalesced_to_guid !== null){
// Don't count or render coalesced jobs // Don't count or render coalesced jobs
continue; continue;
} }
@ -123,25 +189,31 @@ treeherder.directive('thCloneJobs', function(
//Make sure that filtering doesn't effect the resultset counts //Make sure that filtering doesn't effect the resultset counts
//displayed //displayed
if(thJobFilters.showJob(job, resultStatusFilters) === false){ 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; continue;
} }
jobsShown++; jobsShown++;
job.visible = true;
hText = getHoverText(job); hText = getHoverText(job);
key = getJobMapKey(job); key = getJobMapKey(job);
jobStatus = thResultStatusInfo(resultState); jobStatus = thResultStatusInfo(resultState);
jobStatus['key'] = key; jobStatus.key = key;
if(parseInt(job.failure_classification_id) > 1){ if(parseInt(job.failure_classification_id, 10) > 1){
jobStatus['value'] = job.job_type_symbol + '*'; jobStatus.value = job.job_type_symbol + '*';
}else{ }else{
jobStatus['value'] = job.job_type_symbol; jobStatus.value = job.job_type_symbol;
} }
jobStatus['title'] = hText; jobStatus.title = hText;
jobStatus['btnClass'] = jobStatus.btnClass; jobStatus.btnClass = jobStatus.btnClass;
jobBtn = $( jobBtnInterpolator(jobStatus) ); jobBtn = $( jobBtnInterpolator(jobStatus) );
@ -184,6 +256,7 @@ treeherder.directive('thCloneJobs', function(
} }
lastJobElSelected = el; lastJobElSelected = el;
lastJobObjSelected = job;
} }
}; };
@ -203,14 +276,14 @@ treeherder.directive('thCloneJobs', function(
revision = resultset.revisions[i]; revision = resultset.revisions[i];
revision['urlBasePath'] = $rootScope.urlBasePath; revision.urlBasePath = $rootScope.urlBasePath;
revision['currentRepo'] = $rootScope.currentRepo; revision.currentRepo = $rootScope.currentRepo;
userTokens = revision.author.split(/[<>]+/); userTokens = revision.author.split(/[<>]+/);
if (userTokens.length > 1) { 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); revisionHtml = revisionInterpolator(revision);
ulEl.append(revisionHtml); ulEl.append(revisionHtml);
@ -229,7 +302,7 @@ treeherder.directive('thCloneJobs', function(
var rowEl = revisionsEl.parent(); var rowEl = revisionsEl.parent();
rowEl.css('display', 'block'); rowEl.css('display', 'block');
if(revElDisplayState != 'block'){ if(revElDisplayState !== 'block'){
if(jobsElDisplayState === 'block'){ if(jobsElDisplayState === 'block'){
toggleRevisionsSpanOnWithJobs(revisionsEl); toggleRevisionsSpanOnWithJobs(revisionsEl);
@ -264,7 +337,7 @@ treeherder.directive('thCloneJobs', function(
var rowEl = revisionsEl.parent(); var rowEl = revisionsEl.parent();
rowEl.css('display', 'block'); rowEl.css('display', 'block');
if(jobsElDisplayState != 'block'){ if(jobsElDisplayState !== 'block'){
if(revElDisplayState === 'block'){ if(revElDisplayState === 'block'){
toggleJobsSpanOnWithRevisions(jobsEl); toggleJobsSpanOnWithRevisions(jobsEl);
@ -343,7 +416,7 @@ treeherder.directive('thCloneJobs', function(
jgObj = jobGroups[i]; jgObj = jobGroups[i];
jobsShown = 0; jobsShown = 0;
if(jgObj.symbol != '?'){ if(jgObj.symbol !== '?'){
// Job group detected, add job group symbols // Job group detected, add job group symbols
jobGroup = $( jobGroupInterpolator(jobGroups[i]) ); jobGroup = $( jobGroupInterpolator(jobGroups[i]) );
@ -486,7 +559,7 @@ treeherder.directive('thCloneJobs', function(
var jobCounts = thResultStatusObject.getResultStatusObject(); var jobCounts = thResultStatusObject.getResultStatusObject();
var statusKeys = _.keys(jobCounts); var statusKeys = _.keys(jobCounts);
jobCounts['total'] = 0; jobCounts.total = 0;
resultsetId = resultSets[i].id; resultsetId = resultSets[i].id;
@ -511,7 +584,7 @@ treeherder.directive('thCloneJobs', function(
for(k=0; k<statusKeys.length; k++){ for(k=0; k<statusKeys.length; k++){
jobStatus = statusKeys[k]; jobStatus = statusKeys[k];
jobCounts[jobStatus] += statusPerPlatform[jobStatus]; jobCounts[jobStatus] += statusPerPlatform[jobStatus];
jobCounts['total'] += statusPerPlatform[jobStatus]; jobCounts.total += statusPerPlatform[jobStatus];
} }
} }
} }
@ -524,7 +597,7 @@ treeherder.directive('thCloneJobs', function(
angular.forEach(platformData, function(value, platformId){ angular.forEach(platformData, function(value, platformId){
if(value.resultsetId != this.resultset.id){ if(value.resultsetId !== this.resultset.id){
//Confirm we are the correct result set //Confirm we are the correct result set
return; return;
} }
@ -585,13 +658,119 @@ treeherder.directive('thCloneJobs', function(
}, this); }, this);
}; };
var linker = function(scope, element, attrs){ var getNextUnclassifiedFailure = function(currentJob){
//Remove any jquery on() bindings var resultsets = ThResultSetModel.getResultSetsArray($rootScope.repoName);
element.off();
//Register events callback var startWatch = false;
element.on('mousedown', _.bind(jobMouseDown, scope)); if(_.isEmpty(currentJob)){
startWatch = true;
}
var platforms, groups, jobs, r;
superloop:
for(r = 0; r < resultsets.length; r++){
platforms = resultsets[r].platforms;
var p;
for(p = 0; p < platforms.length; p++){
groups = platforms[p].groups;
var g;
for(g = 0; g < groups.length; g++){
jobs = groups[g].jobs;
var j;
for(j = 0; j < jobs.length; 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]);
//Next test failure found
break superloop;
}
}
}
}
}
}
};
var getPreviousUnclassifiedFailure = function(currentJob){
var resultsets = ThResultSetModel.getResultSetsArray($rootScope.repoName);
var startWatch = false;
if(_.isEmpty(currentJob)){
startWatch = true;
}
var platforms, groups, jobs, r;
superloop:
for(r = resultsets.length - 1; r >= 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 //Register rootScope custom event listeners that require
//access to the anguler level resultset scope //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 //Clone the target html
var resultsetAggregateId = thAggregateIds.getResultsetTableId( var resultsetAggregateId = thAggregateIds.getResultsetTableId(
$rootScope.repoName, scope.resultset.id, scope.resultset.revision $rootScope.repoName, scope.resultset.id, scope.resultset.revision
@ -730,557 +922,11 @@ treeherder.directive('thCloneJobs', function(
} }
element.append(targetEl); element.append(targetEl);
} };
return { return {
link:linker, link:linker,
replace:true 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: '<span title="open resultsets for {{authorName}}: {{authorEmail}}">' +
'<a href="{{authorResultsetFilterUrl}}" ' +
'target="_blank">{{authorName}}</a></span>'
};
});
// 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: '<span class="label {{ badgeColorClass}}" ' +
'title="{{ hoverText }}">' +
'<i class="glyphicon glyphicon-star-empty"></i>' +
'</span> {{ 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: '<a class="btn {{ resultsetStateBtn }} th-show-jobs-button pull-left" ' +
'ng-click="isCollapsedResults = !isCollapsedResults">' +
'<i class="{{ icon }}"></i> ' +
'{{ \' jobs\' | showOrHide:isCollapsedResults }}</a>'
};
});
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<what.length; i++){
found = from.indexOf(what[i]);
if(found !== -1){
to.push(from.splice(found, 1)[0]);
}
}
}
scope.move_left = function(){
move_options(scope.rightSelected, scope.rightList, scope.leftList);
};
scope.move_right = function(){
move_options(scope.leftSelected, scope.leftList, scope.rightList);
};
}
}
});
treeherder.directive("thTruncatedList", function($log){
// transforms a list of elements in a shortened list
// with a "more" link
return {
restrict: "E",
scope: {
// number of visible elementrs
visible: "@",
elem_list: "=elements"
},
link: function(scope, element, attrs){
scope.visible = parseInt(scope.visible)
if(typeof scope.visible !== 'number'
|| scope.visible < 0
|| isNaN(scope.visible)){
throw new TypeError("The visible parameter must be a positive number")
}
// cloning the original list to avoid
scope.$watch("elem_list", function(newValue, oldValue){
if(newValue){
var elem_list_clone = angular.copy(newValue);
scope.visible = Math.min(scope.visible, elem_list_clone.length);
var visible_content = elem_list_clone.splice(0, scope.visible);
$(element[0]).empty();
$(element[0]).append(visible_content.join(", "));
if(elem_list_clone.length > 0){
$(element[0]).append(
$("<a></a>")
.attr("title", elem_list_clone.join(", "))
.text(" and "+ elem_list_clone.length+ " others")
.tooltip()
);
}
}
});
}
}
}); });

135
ui/js/directives/main.js Executable file
Просмотреть файл

@ -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<what.length; i++){
found = from.indexOf(what[i]);
if(found !== -1){
to.push(from.splice(found, 1)[0]);
}
}
}
scope.move_left = function(){
move_options(scope.rightSelected, scope.rightList, scope.leftList);
};
scope.move_right = function(){
move_options(scope.leftSelected, scope.leftList, scope.rightList);
};
}
}
});
treeherder.directive("thTruncatedList", function($log){
// transforms a list of elements in a shortened list
// with a "more" link
return {
restrict: "E",
scope: {
// number of visible elementrs
visible: "@",
elem_list: "=elements"
},
link: function(scope, element, attrs){
scope.visible = parseInt(scope.visible)
if(typeof scope.visible !== 'number'
|| scope.visible < 0
|| isNaN(scope.visible)){
throw new TypeError("The visible parameter must be a positive number")
}
// cloning the original list to avoid
scope.$watch("elem_list", function(newValue, oldValue){
if(newValue){
var elem_list_clone = angular.copy(newValue);
scope.visible = Math.min(scope.visible, elem_list_clone.length);
var visible_content = elem_list_clone.splice(0, scope.visible);
$(element[0]).empty();
$(element[0]).append(visible_content.join(", "));
if(elem_list_clone.length > 0){
$(element[0]).append(
$("<a></a>")
.attr("title", elem_list_clone.join(", "))
.text(" and "+ elem_list_clone.length+ " others")
.tooltip()
);
}
}
});
}
}
});

Просмотреть файл

@ -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'
};
});

Просмотреть файл

@ -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: '<span title="open resultsets for {{authorName}}: {{authorEmail}}">' +
'<a href="{{authorResultsetFilterUrl}}" ' +
'target="_blank">{{authorName}}</a></span>'
};
});

Просмотреть файл

@ -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'
};
});

Просмотреть файл

@ -24,4 +24,12 @@ treeherder.filter('platformName', function() {
// if it's not found in Config.js, then return it unchanged. // if it's not found in Config.js, then return it unchanged.
return name; return name;
}; };
}) })
treeherder.filter('stripHtml', function() {
return function(input) {
var str = input || '';
return str.replace(/<\/?[^>]+>/gi, '');
};
})

Просмотреть файл

@ -38,7 +38,7 @@ treeherder.factory('ThBugJobMapModel', function($http, thUrl) {
// an instance method to delete a ThBugJobMap object // an instance method to delete a ThBugJobMap object
ThBugJobMapModel.prototype.delete = function(){ ThBugJobMapModel.prototype.delete = function(){
var pk = this.job_id+"-"+this.bug_id; var pk = this.job_id+"-"+this.bug_id;
return $http.delete(ThBugJobMapModel.get_uri()+pk); return $http.delete(ThBugJobMapModel.get_uri()+pk+"/");
}; };
return ThBugJobMapModel; return ThBugJobMapModel;

Просмотреть файл

Просмотреть файл

@ -1,6 +1,6 @@
'use strict'; 'use strict';
treeherder.factory('ThJobClassificationModel', function($http, $log, thUrl) { treeherder.factory('ThJobClassificationModel', function($http, ThLog, thUrl) {
// ThJobClassificationModel is the js counterpart of note // ThJobClassificationModel is the js counterpart of note
var ThJobClassificationModel = function(data) { var ThJobClassificationModel = function(data) {
@ -39,7 +39,7 @@ treeherder.factory('ThJobClassificationModel', function($http, $log, thUrl) {
// an instance method to delete a ThJobClassificationModel object // an instance method to delete a ThJobClassificationModel object
ThJobClassificationModel.prototype.delete = function(){ ThJobClassificationModel.prototype.delete = function(){
return $http.delete(ThJobClassificationModel.get_uri()+this.id); return $http.delete(ThJobClassificationModel.get_uri()+this.id+"/");
}; };
return ThJobClassificationModel; return ThJobClassificationModel;

Просмотреть файл

@ -1,7 +1,7 @@
'use strict'; 'use strict';
treeherder.factory('ThJobModel', ['$http', '$log', 'thUrl', function($http, $log, thUrl) { treeherder.factory('ThJobModel', function($http, ThLog, thUrl) {
// ThJobArtifactModel is the js counterpart of job_artifact // ThJobModel is the js counterpart of job
var ThJobModel = function(data) { var ThJobModel = function(data) {
// creates a new instance of ThJobArtifactModel // creates a new instance of ThJobArtifactModel
@ -26,10 +26,10 @@ treeherder.factory('ThJobModel', ['$http', '$log', 'thUrl', function($http, $log
ThJobModel.get = function(pk) { ThJobModel.get = function(pk) {
// a static method to retrieve a single instance of ThJobModel // 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 new ThJobModel(response.data);
}); });
}; };
return ThJobModel; return ThJobModel;
}]); });

Просмотреть файл

@ -1,6 +1,6 @@
'use strict'; '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 // ThJobArtifactModel is the js counterpart of job_artifact
var ThJobArtifactModel = function(data) { var ThJobArtifactModel = function(data) {
@ -32,4 +32,4 @@ treeherder.factory('ThJobArtifactModel', ['$http', '$log', 'thUrl', function($ht
}; };
return ThJobArtifactModel; return ThJobArtifactModel;
}]); });

Просмотреть файл

Просмотреть файл

Просмотреть файл

185
ui/js/models/repository.js Normal file
Просмотреть файл

@ -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 <a href="https://treestatus.mozilla.org">treestatus.mozilla.org</a>'
};
});
}
};
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
};
});

Просмотреть файл

@ -1,10 +1,12 @@
'use strict'; 'use strict';
treeherder.factory('ThResultSetModel', treeherder.factory('ThResultSetModel',
['$log', '$rootScope', 'thResultSets', 'thSocket', ['$rootScope', 'thResultSets', 'thSocket',
'ThJobModel', 'thEvents', 'thAggregateIds', 'ThJobModel', 'thEvents', 'thAggregateIds', 'ThLog',
function($log, $rootScope, thResultSets, thSocket, function($rootScope, thResultSets, thSocket,
ThJobModel, thEvents, thAggregateIds) { ThJobModel, thEvents, thAggregateIds, ThLog) {
var $log = new ThLog("ThResultSetModel");
/****** /******
* Handle updating the resultset datamodel based on a queue of jobs * Handle updating the resultset datamodel based on a queue of jobs
@ -112,7 +114,7 @@ treeherder.factory('ThResultSetModel',
var getPlatformKey = function(name, option){ var getPlatformKey = function(name, option){
var key = name; var key = name;
if(option != undefined){ if(option !== undefined){
key += option; key += option;
} }
return key; return key;
@ -144,6 +146,13 @@ treeherder.factory('ThResultSetModel',
repositories[repoName].rsMapOldestTimestamp = rs_obj.push_timestamp; 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 // platforms
for (var pl_i = 0; pl_i < rs_obj.platforms.length; pl_i++) { for (var pl_i = 0; pl_i < rs_obj.platforms.length; pl_i++) {
var pl_obj = rs_obj.platforms[pl_i]; var pl_obj = rs_obj.platforms[pl_i];
@ -191,10 +200,9 @@ treeherder.factory('ThResultSetModel',
repositories[repoName].resultSets.sort(rsCompare); repositories[repoName].resultSets.sort(rsCompare);
$log.debug("oldest job: " + repositories[repoName].jobMapOldestId); $log.debug("oldest job: ", repositories[repoName].jobMapOldestId);
$log.debug("oldest result set: " + repositories[repoName].rsMapOldestTimestamp); $log.debug("oldest result set: ", repositories[repoName].rsMapOldestTimestamp);
$log.debug("done mapping:"); $log.debug("done mapping:", repositories[repoName].rsMap);
$log.debug(repositories[repoName].rsMap);
}; };
/** /**
@ -226,7 +234,7 @@ treeherder.factory('ThResultSetModel',
var pl_obj = { var pl_obj = {
name: newJob.platform, name: newJob.platform,
option: newJob.platform_opt, option: newJob.platform_option,
groups: [] groups: []
}; };
@ -308,28 +316,31 @@ treeherder.factory('ThResultSetModel',
} }
if (jobFetchList.length > 0) { if (jobFetchList.length > 0) {
$log.debug("processing jobFetchList"); $log.debug("processing jobFetchList", jobFetchList);
$log.debug(jobFetchList);
// make an ajax call to get the job details // make an ajax call to get the job details
fetchJobs(repoName, jobFetchList);
ThJobModel.get_list({
id__in: jobFetchList.join()
}).then(
_.bind(updateJobs, $rootScope, repoName),
function(data) {
$log.error("Error fetching jobUpdateQueue: " + data);
});
} }
}; };
/**
* 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 aggregateJobPlatform = function(repoName, job, platformData){
var resultsetId, platformName, platformOption, platformAggregateId, var resultsetId, platformName, platformOption, platformAggregateId,
platformKey, jobUpdated, resultsetAggregateId, revision, platformKey, jobUpdated, resultsetAggregateId, revision,
jobGroups; jobGroups;
console.log('aggregating job platform');
console.log(job);
jobUpdated = updateJob(repoName, job); jobUpdated = updateJob(repoName, job);
//the job was not updated or added to the model, don't include it //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; resultsetId = job.result_set_id;
platformName = job.platform; platformName = job.platform;
platformOption = job.platform_opt; platformOption = job.platform_option;
if(_.isEmpty(repositories[repoName].rsMap[ resultsetId ])){ if(_.isEmpty(repositories[repoName].rsMap[ resultsetId ])){
//We don't have this resultset //We don't have this resultset
@ -351,7 +362,7 @@ treeherder.factory('ThResultSetModel',
repoName, repoName,
job.result_set_id, job.result_set_id,
job.platform, job.platform,
job.platform_opt job.platform_option
); );
if(!platformData[platformAggregateId]){ if(!platformData[platformAggregateId]){
@ -366,6 +377,7 @@ treeherder.factory('ThResultSetModel',
platformKey = getPlatformKey(platformName, platformOption); platformKey = getPlatformKey(platformName, platformOption);
$log.debug("aggregateJobPlatform", repoName, resultsetId, platformKey, repositories);
jobGroups = repositories[repoName].rsMap[resultsetId].platforms[platformKey].pl_obj.groups; jobGroups = repositories[repoName].rsMap[resultsetId].platforms[platformKey].pl_obj.groups;
platformData[platformAggregateId] = { platformData[platformAggregateId] = {
platformName:platformName, platformName:platformName,
@ -389,7 +401,7 @@ treeherder.factory('ThResultSetModel',
*/ */
var updateJobs = function(repoName, jobList) { 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 = {}; var platformData = {};
@ -450,11 +462,11 @@ treeherder.factory('ThResultSetModel',
} }
if (loadedJob) { if (loadedJob) {
$log.debug("updating existing job"); $log.debug("updating existing job", loadedJob, newJob);
_.extend(loadedJob, newJob); _.extend(loadedJob, newJob);
} else { } else {
// this job is not yet in the model or the map. add it to both // 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); var grpMapElement = getOrCreateGroup(repoName, newJob);
@ -486,7 +498,7 @@ treeherder.factory('ThResultSetModel',
var added = []; var added = [];
for (var i = data.length - 1; i > -1; i--) { for (var i = data.length - 1; i > -1; i--) {
if (data[i].push_timestamp > repositories[repoName].rsMapOldestTimestamp) { 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]); repositories[repoName].resultSets.push(data[i]);
added.push(data[i]); added.push(data[i]);
} else { } else {
@ -502,7 +514,7 @@ treeherder.factory('ThResultSetModel',
var appendResultSets = function(repoName, data) { var appendResultSets = function(repoName, data) {
if(data.length > 0){ if(data.length > 0){
repositories[repoName].rsOffsetId = data[ data.length - 1 ].id;
Array.prototype.push.apply( Array.prototype.push.apply(
repositories[repoName].resultSets, data repositories[repoName].resultSets, data
@ -556,7 +568,7 @@ treeherder.factory('ThResultSetModel',
*/ */
if(resultsetList.length > 0){ if(resultsetList.length > 0){
repositories[repoName].loadingStatus.prepending = true; repositories[repoName].loadingStatus.prepending = true;
thResultSets.getResultSets(0, resultsetlist.length, resultsetlist). thResultSets.getResultSets(0, resultsetList.length, resultsetList).
success( _.bind(prependResultSets, $rootScope, repoName) ); success( _.bind(prependResultSets, $rootScope, repoName) );
} }
}; };
@ -596,6 +608,8 @@ treeherder.factory('ThResultSetModel',
fetchResultSets: fetchResultSets, fetchResultSets: fetchResultSets,
fetchJobs: fetchJobs,
aggregateJobPlatform: aggregateJobPlatform aggregateJobPlatform: aggregateJobPlatform
}; };

Просмотреть файл

Просмотреть файл

@ -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() { treeherder.provider('thResultStatusList', function() {
this.$get = function() { this.$get = function() {
return ['success', 'testfailed', 'busted', 'exception', 'retry', 'running', 'pending']; return ['success', 'testfailed', 'busted', 'exception', 'retry', 'running', 'pending'];
}; };
}); });
treeherder.provider('thResultStatus', function() { treeherder.provider('thResultStatus', function() {
this.$get = function() { this.$get = function() {
return function(job) { return function(job) {
@ -57,6 +27,7 @@ treeherder.provider('thResultStatus', function() {
}; };
}; };
}); });
treeherder.provider('thResultStatusObject', function() { treeherder.provider('thResultStatusObject', function() {
var getResultStatusObject = function(){ var getResultStatusObject = function(){
return { return {
@ -203,6 +174,9 @@ treeherder.provider('thEvents', function() {
// fired (surprisingly) when a job is clicked // fired (surprisingly) when a job is clicked
jobClick: "job-click-EVT", jobClick: "job-click-EVT",
// fired when the job details are loaded
jobDetailLoaded: "job-detail-loaded-EVT",
// fired when a job is shift-clicked // fired when a job is shift-clicked
jobPin: "job-pin-EVT", jobPin: "job-pin-EVT",
@ -230,9 +204,21 @@ treeherder.provider('thEvents', function() {
toggleJobs: "toggle-jobs-EVT", toggleJobs: "toggle-jobs-EVT",
toggleUnclassifiedFailures: "toggle-unclassified-failures-EVT",
selectNextUnclassifiedFailure: "next-unclassified-failure-EVT",
selectPreviousUnclassifiedFailure: "previous-unclassified-failure-EVT",
searchPage: "search-page-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"
}; };
}; };
}); });

Просмотреть файл

@ -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
};
});

Просмотреть файл

@ -21,7 +21,11 @@
* Each field is AND'ed so that, if a field exists in ``filters`` then the job * 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. * 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 * 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. * means it must have a value set, ``false`` means it must be null.
*/ */
var checkFilter = function(field, job, resultStatusList) { 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) { if (field === api.resultStatus) {
// resultStatus is a special case that spans two job fields
var filterList = resultStatusList || filters[field].values; var filterList = resultStatusList || filters[field].values;
return _.contains(filterList, job.result) || return _.contains(filterList, job.result) ||
_.contains(filterList, job.state); _.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 { } else {
var jobFieldValue = getJobFieldValue(job, field); var jobFieldValue = getJobFieldValue(job, field);
if (_.isUndefined(jobFieldValue)) { 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; return true;
} }
$log.debug(field + ": " + JSON.stringify(job)); $log.debug("jobField filter", field, job);
switch (filters[field].matchType) { switch (filters[field].matchType) {
case api.matchType.isnull: case api.matchType.isnull:
jobFieldValue = !_.isNull(jobFieldValue); jobFieldValue = !_.isNull(jobFieldValue);
@ -117,7 +131,7 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope
value = value.toLowerCase(); value = value.toLowerCase();
} }
if (filters.hasOwnProperty(field)) { if (filters.hasOwnProperty(field)) {
if (!_.contains(filters[field], value)) { if (!_.contains(filters[field].values, value)) {
filters[field].values.push(value); filters[field].values.push(value);
filters[field].matchType = matchType; filters[field].matchType = matchType;
} }
@ -128,8 +142,11 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope
removeWhenEmpty: true 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) { removeFilter: function(field, value) {
if (filters.hasOwnProperty(field)) { if (filters.hasOwnProperty(field)) {
@ -139,7 +156,7 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope
} }
var idx = filters[field].values.indexOf(value); var idx = filters[field].values.indexOf(value);
if(idx > -1) { if(idx > -1) {
$log.debug("removing " + value); $log.debug("removing ", value);
filters[field].values.splice(idx, 1); 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) { if (filters[field].removeWhenEmpty && filters[field].values.length === 0) {
delete filters[field]; delete filters[field];
} }
$log.debug(filters);
filterKeys = _.keys(filters);
$log.debug("filters", filters);
}, },
/** /**
* used mostly for resultStatus doing group toggles * 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 * @param add - true if adding, false if removing
*/ */
toggleFilters: function(field, values, add) { toggleFilters: function(field, values, add) {
$log.debug("toggling: " + add); $log.debug("toggling: ", add);
var action = add? api.addFilter: api.removeFilter; var action = add? api.addFilter: api.removeFilter;
for (var i = 0; i < values.length; i++) { for (var i = 0; i < values.length; i++) {
action(field, values[i]); action(field, values[i]);
@ -182,7 +201,7 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope
return false; return false;
} }
} }
if($rootScope.searchQuery != ""){ if(typeof $rootScope.searchQuery === 'string'){
//Confirm job matches search query //Confirm job matches search query
if(job.searchableStr.toLowerCase().indexOf( if(job.searchableStr.toLowerCase().indexOf(
$rootScope.searchQuery.toLowerCase() $rootScope.searchQuery.toLowerCase()
@ -206,6 +225,31 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope
getFilters: function() { getFilters: function() {
return filters; 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 // CONSTANTS
failure_classification_id: "failure_classification_id", failure_classification_id: "failure_classification_id",
@ -213,7 +257,8 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope
matchType: { matchType: {
exactstr: 0, exactstr: 0,
substr: 1, substr: 1,
isnull: 2 isnull: 2,
bool: 3
} }
}; };
@ -225,7 +270,7 @@ treeherder.factory('thJobFilters', function(thResultStatusList, $log, $rootScope
removeWhenEmpty: false removeWhenEmpty: false
}, },
failure_classification_id: { failure_classification_id: {
matchType: api.matchType.isnull, matchType: api.matchType.bool,
values: [true, false], values: [true, false],
removeWhenEmpty: false removeWhenEmpty: false
} }

87
ui/js/services/log.js Normal file
Просмотреть файл

@ -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;
}]);
}]);

Просмотреть файл

@ -1,7 +1,7 @@
'use strict'; 'use strict';
/* Services */ /* Services */
treeherder.factory('thUrl',['$rootScope', 'thServiceDomain', '$log', function($rootScope, thServiceDomain, $log) { treeherder.factory('thUrl',['$rootScope', 'thServiceDomain', 'ThLog', function($rootScope, thServiceDomain, ThLog) {
var thUrl = { var thUrl = {
getRootUrl: function(uri) { 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()); var socket = io.connect(thUrl.getSocketEventUrl());
socket.on('connect', function () { socket.on('connect', function () {
$log.debug('socketio connected'); $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 * BrowserId is a wrapper for the persona authentication service
@ -180,9 +182,11 @@ treeherder.factory('BrowserId', function($http, $q, $log, thServiceDomain){
return browserid; return browserid;
}); });
treeherder.factory('thNotify', function($timeout, $log){ treeherder.factory('thNotify', function($timeout, ThLog){
//a growl-like notification system //a growl-like notification system
var $log = new ThLog("thNotify");
var thNotify = { var thNotify = {
// message queue // message queue
notifications: [], notifications: [],
@ -194,8 +198,7 @@ treeherder.factory('thNotify', function($timeout, $log){
* after a while or not * after a while or not
*/ */
send: function(message, severity, sticky){ send: function(message, severity, sticky){
$log.debug("received message"); $log.debug("received message", message);
$log.debug(message);
var severity = severity || 'info'; var severity = severity || 'info';
var sticky = sticky || false; var sticky = sticky || false;
thNotify.notifications.push({ thNotify.notifications.push({

Просмотреть файл

@ -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;
}]);

Просмотреть файл

@ -2,7 +2,9 @@
treeherder.factory('thPinboard', treeherder.factory('thPinboard',
function($http, thUrl, ThJobClassificationModel, $rootScope, function($http, thUrl, ThJobClassificationModel, $rootScope,
thEvents, ThBugJobMapModel, thNotify) { thEvents, ThBugJobMapModel, thNotify, ThLog) {
var $log = new ThLog("thPinboard");
var pinnedJobs = {}; var pinnedJobs = {};
var relatedBugs = {}; var relatedBugs = {};
@ -10,15 +12,18 @@ treeherder.factory('thPinboard',
var saveClassification = function(job) { var saveClassification = function(job) {
var classification = new ThJobClassificationModel(this); 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.job_id = job.id;
classification.create(). classification.create().
success(function(data) { success(function(data) {
thNotify.send("classification saved for " + job.platform + ": " + job.job_type_name, "success"); thNotify.send("classification saved for " + job.platform + ": " + job.job_type_name, "success");
}).error(function(data) { }).error(function(data) {
thNotify.send("error saving classification for " + job.platform + ": " + job.job_type_name, "danger"); thNotify.send("error saving classification for " + job.platform + ": " + job.job_type_name, "danger");
}); });
}
}; };
var saveBugs = function(job) { var saveBugs = function(job) {
@ -40,8 +45,12 @@ treeherder.factory('thPinboard',
var api = { var api = {
pinJob: function(job) { pinJob: function(job) {
pinnedJobs[job.id] = job; if (api.spaceRemaining() > 0) {
api.count.numPinnedJobs = _.size(pinnedJobs); 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) { unPinJob: function(id) {
@ -59,8 +68,11 @@ treeherder.factory('thPinboard',
}, },
addBug: function(bug) { addBug: function(bug) {
$log.debug("adding bug ", bug);
relatedBugs[bug.id] = bug; relatedBugs[bug.id] = bug;
api.count.numRelatedBugs = _.size(relatedBugs); api.count.numRelatedBugs = _.size(relatedBugs);
$log.debug("related bugs", relatedBugs);
}, },
removeBug: function(id) { removeBug: function(id) {
@ -105,23 +117,26 @@ treeherder.factory('thPinboard',
// save bug associations only on all pinned jobs // save bug associations only on all pinned jobs
saveBugsOnly: function() { saveBugsOnly: function() {
if (!_.size(relatedBugs)) { _.each(pinnedJobs, saveBugs);
thNotify.send("no bug associations to save"); $rootScope.$broadcast(thEvents.bugsAssociated, {jobs: pinnedJobs});
} else {
_.each(pinnedJobs, saveBugs);
$rootScope.$broadcast(thEvents.bugsAssociated, {jobs: pinnedJobs});
}
}, },
hasPinnedJobs: function() { hasPinnedJobs: function() {
return !_.isEmpty(pinnedJobs); return !_.isEmpty(pinnedJobs);
}, },
spaceRemaining: function() {
return api.maxNumPinned - api.count.numPinnedJobs;
},
pinnedJobs: pinnedJobs, pinnedJobs: pinnedJobs,
relatedBugs: relatedBugs, relatedBugs: relatedBugs,
count: { count: {
numPinnedJobs: 0, numPinnedJobs: 0,
numRelatedBugs: 0 numRelatedBugs: 0
} },
// not sure what this should be, but we need some limit, I think.
maxNumPinned: 500
}; };
return api; return api;

Просмотреть файл

@ -1,7 +1,6 @@
'use strict'; 'use strict';
treeherder.factory('thResultSets', treeherder.factory('thResultSets',
['$http', '$location', 'thUrl', 'thServiceDomain',
function($http, $location, thUrl, thServiceDomain) { function($http, $location, thUrl, thServiceDomain) {
// get the resultsets for this repo // get the resultsets for this repo
@ -42,4 +41,4 @@ treeherder.factory('thResultSets',
return $http.get(thServiceDomain + uri, {params: {format: "json"}}); return $http.get(thServiceDomain + uri, {params: {format: "json"}});
} }
}; };
}]); });

Просмотреть файл

@ -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
};
});

Просмотреть файл

@ -9,39 +9,37 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<dl class="dl-horizontal"> <table class="table table-condensed">
<span ng-repeat="(label, value) in artifact.header"> <tr ng-repeat="(label, value) in artifact.header">
<dt class="label label-info">{{label}}</dt> <th>{{label}}</th><td>{{value}}</td>
<dd class="">{{value}}</dd> </tr>
</span> </table>
</dl> <p>Select one of these steps to see more details:</p>
<div class="row" > <div ng-repeat="step in artifact.step_data.steps"
<div ng-repeat="step in artifact.step_data.steps" ng-class="{'btn-warning': (step.error_count > 0), 'btn-success': (step.error_count == 0)}"
ng-class="{'btn-warning': (step.error_count > 0), 'btn-success': (step.error_count == 0)}" ng-click="displayLog(step)"
ng-click="displayLog(step)" class="btn btn-block logviewer-step clearfix">
class="btn btn-block clearfix"> <span class="pull-left clearfix">{{step.order+1}}. {{step.name}}</span>
<span class="pull-left clearfix">{{step.order+1}}. {{step.name}}</span> <span ng-init="time=formatTime(step.duration)"
<span ng-init="time=formatTime(step.duration)" ng-mouseover="time=displayTime(step.started, step.finished)"
ng-mouseover="time=displayTime(step.started, step.finished)" ng-mouseleave="time=formatTime(step.duration)"
ng-mouseleave="time=formatTime(step.duration)" class="pull-right clearfix">{{time}}</span>
class="pull-right clearfix">{{time}}</span> <div ng-switch on="(step.error_count > 0)">
<div ng-switch on="(step.error_count > 0)"> <p ng-switch-when="true" class="">
<p ng-switch-when="true" class=""> <div ng-repeat="error in step.errors"
<div ng-repeat="error in step.errors" ng-mouseover="check=(step==displayedStep)"
ng-mouseover="check=(step==displayedStep)" ng-mouseleave="check=false"
ng-mouseleave="check=false" ng-class="{'lv-line-highlight': check}"
ng-class="{'lv-line-highlight': check}" ng-click="scrollTo(step, error.linenumber);
ng-click="scrollTo(step, error.linenumber); $event.stopPropagation()"
$event.stopPropagation()" class="text-left pull-left lv-error-line">
class="text-left pull-left lv-error-line"> <span class="label label-default lv-line-no">{{error.linenumber}}</span>
<span class="label label-default lv-line-no">{{error.linenumber}}</span> <span title="{{error.line}}">{{error.line | limitTo: 67}}</span><span ng-if="error.line.length > 70">...</span>
<span>{{error.line}}</span> </div>
</div> </p>
</p>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6 lv-log-container container well" <div class="col-md-6 lv-log-container container well"
id="lv-log-container"> id="lv-log-container">
<div ng-repeat="lv_line in displayedStep.logPieces" <div ng-repeat="lv_line in displayedStep.logPieces"
@ -65,7 +63,8 @@
<script src="js/config/local.conf.js"></script> <script src="js/config/local.conf.js"></script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
<script src="js/services/main.js"></script> <script src="js/services/main.js"></script>
<script src="js/services/models/job_artifact.js"></script> <script src="js/services/log.js"></script>
<script src="js/models/job_artifact.js"></script>
<script src="js/providers.js"></script> <script src="js/providers.js"></script>
<script src="js/controllers/logviewer.js"></script> <script src="js/controllers/logviewer.js"></script>
</body> </body>

Просмотреть файл

@ -6,7 +6,7 @@
<ul class="dropdown-menu pull-left"> <ul class="dropdown-menu pull-left">
<li><a target="_blank" href="https://tbpl.mozilla.org/mcmerge/?cset={{resultset.revision}}&tree={{repoName}}">m-mcMerge</a></li> <li><a target="_blank" href="https://tbpl.mozilla.org/mcmerge/?cset={{resultset.revision}}&tree={{repoName}}">m-mcMerge</a></li>
<li><a target="_blank" href="https://secure.pub.build.mozilla.org/buildapi/self-serve/{{repoName}}/rev/{{resultset.revision}}">BuildAPI</a></li> <li><a target="_blank" href="https://secure.pub.build.mozilla.org/buildapi/self-serve/{{repoName}}/rev/{{resultset.revision}}">BuildAPI</a></li>
<li><a target="_blank" href="" data-toggle="modal" data-target="#revisionListModal">Changeset URL List</a></li> <li class="hidden"><a target="_blank" href="" data-toggle="modal" data-target="#revisionListModal">Changeset URL List</a></li>
<li class="hidden"><a href="" ng-disabled="true">Cancel All Jobs</a></li> <li class="hidden"><a href="" ng-disabled="true">Cancel All Jobs</a></li>
</ul> </ul>
</span> </span>

Просмотреть файл

@ -1,7 +1,16 @@
<div id="filter" <div id="filter"
class="th-top-nav-options-panel model-body" class="th-top-nav-options-panel model-body"
ng-controller="StatusFilterPanelCtrl"> ng-controller="FilterPanelCtrl">
<div class="nav-panel-help-text">Specify which jobs to show based on this filter criteria.</div> <div class="nav-panel-help-text">Specify which jobs to show based on this filter criteria.
<span class="pull-right">
<span class="btn btn-default btn-xs"
title="set filtering to show only unclassified failures"
ng-click="showUnclassifiedFailures()"><i class="fa fa-star-o"></i> unclassified failures</span>
<span class="btn btn-default btn-xs"
title="pin all jobs that pass the global filters"
ng-click="pinAllShownJobs()"><i class="glyphicon glyphicon-pushpin"></i> pin all showing</span>
</span>
</div>
<!-- result status filters --> <!-- result status filters -->
<span ng-repeat="group in filterGroups" <span ng-repeat="group in filterGroups"

Просмотреть файл

@ -23,44 +23,43 @@
ng-show="isSheriffPanelShowing"></i> ng-show="isSheriffPanelShowing"></i>
</span> </span>
</span> </span>
<span class="btn btn-view-nav" <span class="pull-right">
ng-class="{'active': (isRepoPanelShowing)}" <span class="btn btn-view-nav"
ng-click="isRepoPanelShowing=!isRepoPanelShowing"><span>repos</span> ng-class="{'active': (isRepoPanelShowing)}"
<i class="glyphicon glyphicon-chevron-down lightgray" ng-click="isRepoPanelShowing=!isRepoPanelShowing"><span>Repos</span>
ng-hide="isRepoPanelShowing"></i> <i class="glyphicon glyphicon-chevron-down lightgray"
<i class="glyphicon glyphicon-chevron-up lightgray" ng-hide="isRepoPanelShowing"></i>
ng-show="isRepoPanelShowing"></i> <i class="glyphicon glyphicon-chevron-up lightgray"
</span> ng-show="isRepoPanelShowing"></i>
<span class="btn btn-view-nav" </span>
ng-class="{'active': (isFilterPanelShowing)}" <span class="btn btn-view-nav"
ng-click="isFilterPanelShowing=!isFilterPanelShowing"><span>filters</span> ng-class="{'active': (isFilterPanelShowing)}"
<i class="glyphicon glyphicon-chevron-down lightgray" ng-click="isFilterPanelShowing=!isFilterPanelShowing"><span>Filters</span>
ng-hide="isFilterPanelShowing"></i> <i class="glyphicon glyphicon-chevron-down lightgray"
<i class="glyphicon glyphicon-chevron-up lightgray" ng-hide="isFilterPanelShowing"></i>
ng-show="isFilterPanelShowing"></i> <i class="glyphicon glyphicon-chevron-up lightgray"
</span> ng-show="isFilterPanelShowing"></i>
<a class="btn btn-view-nav" href="help.html" target="_blank">help</a> </span>
<span class="nav-text white th-username">{{user.email}}</span> <a class="btn btn-view-nav" href="help.html" target="_blank">Help</a>
<span class="nav-text white th-username">{{user.email}}</span>
<!--TODO: change this condition to enable the settings panel--> <!--TODO: change this condition to enable the settings panel-->
<span ng-show="false" class="btn btn-view-nav" <span ng-show="false" class="btn btn-view-nav"
ng-class="{'active': (isSettingsPanelShowing)}" ng-class="{'active': (isSettingsPanelShowing)}"
ng-click="isSettingsPanelShowing=!isSettingsPanelShowing"><span>Settings</span> ng-click="isSettingsPanelShowing=!isSettingsPanelShowing"><span>Settings</span>
<i class="glyphicon glyphicon-chevron-down lightgray" <i class="glyphicon glyphicon-chevron-down lightgray"
ng-hide="isSettingsPanelShowing"></i> ng-hide="isSettingsPanelShowing"></i>
<i class="glyphicon glyphicon-chevron-up lightgray" <i class="glyphicon glyphicon-chevron-up lightgray"
ng-show="isSettingsPanelShowing"></i> ng-show="isSettingsPanelShowing"></i>
</span>
<persona-buttons></persona-buttons>
</span> </span>
<persona-buttons></persona-buttons>
</span>
</div>
<ng-include class="watched-repo-navbar" src="'partials/thWatchedRepoPanel.html'" ng-show="locationPath==='jobs'">
</ng-include>
</div> </div>
<th-watched-repo-panel ng-show="locationPath==='jobs'"></th-watched-repo-panel> <ng-include src="'partials/thSheriffPanel.html'" ng-show="isSheriffPanelShowing"></ng-include>
<div ng-show="isFilterPanelShowing"> <ng-include src="'partials/thFilterPanel.html'" ng-show="isFilterPanelShowing"></ng-include>
<th-status-filter-panel></th-status-filter-panel> <ng-include src="'partials/thRepoPanel.html'" ng-show="isRepoPanelShowing"></ng-include>
</div>
<th-repo-panel ng-show="isRepoPanelShowing"></th-repo-panel>
<th-sheriff-panel ng-show="isSheriffPanelShowing"></th-sheriff-panel>
<th-settings-panel ng-show="isSettingsPanelShowing"></th-settings-panel>
</nav> </nav>

Просмотреть файл

@ -1,9 +0,0 @@
<span class="btn job-btn {{ job.display.btnClass }}"
ng-class="{'btn-lg selected-job': (selectedJob==job), 'btn-xs': (selectedJob!=job)}"
title="{{ hoverText }}"
ng-click="viewJob(job)"
ng-hide="job.job_coalesced_to_guid"
ng-right-click="viewLog(job.resource_uri)"
data-job-id="{{ job.job_id }}">
{{ job.job_type_symbol }}
</span>

Просмотреть файл

@ -1,4 +1,5 @@
<div id="pinboard-panel" <div id="pinboard-panel"
ng-show="hasPinnedJobs()"> ng-show="hasPinnedJobs()">
<div class="bottom-panel-title"><i class="glyphicon glyphicon-pushpin"></i> pinboard</div> <div class="bottom-panel-title"><i class="glyphicon glyphicon-pushpin"></i> pinboard</div>
<div class="panel shadowed-panel pinboard-shadowed-panel"> <div class="panel shadowed-panel pinboard-shadowed-panel">
@ -9,6 +10,7 @@
<div id="pinboard-related-bugs-panel"> <div id="pinboard-related-bugs-panel">
<div class="bottom-panel-title"><i class="fa fa-bug"></i> related bugs <div class="bottom-panel-title"><i class="fa fa-bug"></i> related bugs
<a ng-click="toggleEnterBugNumber()" <a ng-click="toggleEnterBugNumber()"
class="click-able-icon lightgray"
title="type in a bug number"><i class="fa fa-plus"></i> title="type in a bug number"><i class="fa fa-plus"></i>
</a> </a>
</div> </div>
@ -16,13 +18,13 @@
<form ng-submit="saveEnteredBugNumber()"> <form ng-submit="saveEnteredBugNumber()">
<input type="number" <input type="number"
ng-show="enteringBugNumber" ng-show="enteringBugNumber"
ng-model="newEnteredBugNumber" ng-model="$parent.newEnteredBugNumber"
placeholder="enter bug number" placeholder="enter bug number"
numbers-only="numbers-only" numbers-only="numbers-only"
focus-me="focusInput"> focus-me="focusInput">
</form> </form>
<span ng-repeat="bug in relatedBugs"> <span ng-repeat="bug in relatedBugs">
<th-related-bug></th-related-bug> <th-related-bug-queued></th-related-bug-queued>
</span> </span>
</div> </div>
</div> </div>

Просмотреть файл

Просмотреть файл

@ -0,0 +1,9 @@
<span class="btn-group">
<a class="btn btn-default btn-xs pinboard-related-bug-button"
href="https://bugzilla.mozilla.org/show_bug.cgi?id={{bug.bug_id}}"
target="_blank"
>{{bug.bug_id}}</a>
<span class="btn btn-ltgray btn-xs pinned-job-close-btn"
ng-click="deleteBug(bug)"
title="delete relation to bug {{bug.bug_id}}"><i class="fa fa-trash-o"></i></span>
</span>

Просмотреть файл

@ -0,0 +1,38 @@
<ul class="dropdown-menu" role="menu">
<li ng-show="reason" class="watched-repo-dropdown-item">
<span ng-bind-html="reason"></span>
</li>
<li class="divider" ng-show="reason && message_of_the_day"></li>
<li ng-show="message_of_the_day" class="watched-repo-dropdown-item">
<span ng-bind-html="message_of_the_day"></span>
</li>
<li class="divider" ng-show="reason || message_of_the_day"></li>
<li class="watched-repo-dropdown-item">
<a href="irc://irc.mozilla.org/#developers" target="_blank">#developers</a>
</li>
<li class="watched-repo-dropdown-item">
<a href="https://www.google.com/calendar/embed?src=aelh98g866kuc80d5nbfqo6u54%40group.calendar.google.com" target="_blank">schedule</a>
</li>
<li class="watched-repo-dropdown-item">
<a href="https://treestatus.mozilla.org/{{name}}" target="_blank">tree status</a>
</li>
<li class="watched-repo-dropdown-item">
<a href="{{pushlog}}" target="_blank">pushlog</a>
</li>
<li class="watched-repo-dropdown-item">
<a href="https://secure.pub.build.mozilla.org/clobberer/?branch={{name}}" target="_blank">clobberer</a>
</li>
<li class="watched-repo-dropdown-item">
<a href="" ng-click="unwatchRepo(name)">unwatch</a>
</li>
</ul>

Просмотреть файл

@ -13,12 +13,19 @@
<div class="th-repo-group"> <div class="th-repo-group">
<span ng-repeat="repo in group | orderBy:'name'" <span ng-repeat="repo in group | orderBy:'name'"
class="th-repo-group-items"> class="th-repo-group-items">
<input type="checkbox" <input type="checkbox"
ng-model="watchedRepos[repo.name]" ng-checked="watchedRepos[repo.name].isWatched"
ng-change="saveWatchedRepos()"> ng-click="toggleRepo(repo.name)">
<a href="" class="repo-link" <a href="" class="repo-link"
ng-click="changeRepo(repo.name)">{{repo.name}} ng-click="changeRepo(repo.name)">{{repo.name}}
</a> </a>
<span class="dropdown-toggle"
data-toggle="dropdown"
title="{{repo.name}} info"
ng-click="setDropDownPull($event)">
<span class="fa fa-caret-down"></span>
</span>
<th-repo-drop-down name="{{repo.name}}"></th-repo-drop-down>
</span> </span>
</div> </div>
</div> </div>

Просмотреть файл

@ -0,0 +1,27 @@
<span class="btn-group"
ng-show="repoData.isWatched">
<button ng-class="{'active': name===repoName}"
ng-click="changeRepo(name)"
type="button"
title="{{titleText|stripHtml}}"
class="btn btn-sm btn-view-nav">
<i class="fa {{statusIcon}} {{statusColor}}"></i> {{name}}
<span class="badge"
ng-show="repoData.unclassifiedFailureCount > 0">
{{repoData.unclassifiedFailureCount}}
</span>
</button>
<button class="btn btn-sm btn-view-nav dropdown-toggle"
ng-class="{'active': name===repoName}"
data-toggle="dropdown"
title="{{name}} info"
type="button"
ng-click="setDropDownPull($event)">
<span class="fa fa-info-circle"></span>
</button>
<th-repo-drop-down name="{{name}}"
reason="{{repoData.treeStatus.reason}}"
message_of_the_day="{{repoData.treeStatus.message_of_the_day}}">
</th-repo-drop-down>
</span>

Просмотреть файл

@ -1,17 +1,13 @@
<div class="th-context-navbar" > <div class="th-context-navbar watched-repo-navbar clearfix" >
<span ng-repeat="(repo, isVisible) in watchedRepos" <th-watched-repo ng-repeat="(name, repoData) in watchedRepos"></th-watched-repo>
ng-class="{'active': repo===repoName}" <span class="navbar-right">
ng-click="changeRepo(repo)"
ng-show="isVisible"
type="button"
class="btn btn-sm btn-view-nav">{{repo}} <span class="badge" ng-show="failures[repo] > 0">{{failures[repo]}}</span></span>
<span class="pull-right">
<span> <span>
<form role="search" class="form-inline"> <form role="search" class="form-inline">
<span class="label label-primary"><span ng-bind="pinboardCount.numPinnedJobs"></span> pinned jobs</span> <span class="label label-primary"><span ng-bind="pinboardCount.numPinnedJobs"></span> pinned jobs</span>
<span class="label label-primary">0 unclassified</span> <span class="label label-primary">0 unclassified</span>
<div ng-controller="SearchCtrl" class="form-group form-inline"> <div ng-controller="SearchCtrl" class="form-group form-inline">
<input ng-model="searchQuery" ng-keyup="search($event)" type="text" <input id="platform-job-text-search-field"
ng-model="searchQuery" ng-keyup="search($event)" type="text"
class="form-control input-sm" class="form-control input-sm"
placeholder="Filter platforms & jobs"> placeholder="Filter platforms & jobs">
</div> </div>

Просмотреть файл

@ -1,12 +1,53 @@
"use strict"; "use strict";
treeherder.controller('AnnotationsPluginCtrl', treeherder.controller('AnnotationsPluginCtrl',
function AnnotationsPluginCtrl($scope, $log) { function AnnotationsPluginCtrl($scope, $rootScope, ThLog, ThJobClassificationModel,
thNotify, thEvents, ThResultSetModel, ThBugJobMapModel) {
var $log = new ThLog(this.constructor.name);
$log.debug("annotations plugin initialized"); $log.debug("annotations plugin initialized");
$scope.$watch('classifications', function(newValue, oldValue){ $scope.$watch('classifications', function(newValue, oldValue){
$scope.tabs.annotations.num_items = newValue ? $scope.classifications.length : 0; $scope.tabs.annotations.num_items = newValue ? $scope.classifications.length : 0;
}, true); }, true);
$scope.deleteClassification = function(classification) {
var jcModel = new ThJobClassificationModel(classification);
jcModel.delete()
.then(
function(){
thNotify.send("Classification successfully deleted", "success", false);
var jobs = {};
jobs[$scope.selectedJob.id] = $scope.selectedJob;
// also be sure the job object in question gets updated to the latest
// classification state (in case one was added or removed).
ThResultSetModel.fetchJobs($scope.repoName, [$scope.job.id]);
$rootScope.$broadcast(thEvents.jobsClassified, {jobs: jobs});
},
function(){
thNotify.send("Classification deletion failed", "danger", true);
}
);
};
$scope.deleteBug = function(bug) {
var bjmModel = new ThBugJobMapModel(bug);
bjmModel.delete()
.then(
function(){
thNotify.send("Association to bug " + bug.bug_id + " successfully deleted", "success", false);
var jobs = {};
jobs[$scope.selectedJob.id] = $scope.selectedJob;
$rootScope.$broadcast(thEvents.bugsAssociated, {jobs: jobs});
},
function(){
thNotify.send("Association to bug " + bug.bug_id + " deletion failed", "danger", true);
}
);
};
} }
); );

Просмотреть файл

@ -1,5 +1,5 @@
<div ng-controller="AnnotationsPluginCtrl" class="row annotations-panel"> <div ng-controller="AnnotationsPluginCtrl" class="row annotations-panel">
<div class="col-xs-11 classifications-panel"> <div class="col-xs-10 classifications-panel">
<table class="table-condensed table-hover"> <table class="table-condensed table-hover">
<thead> <thead>
<tr><th>Datetime</th><th>Author</th><th>Failure type</th><th>Note</th></tr> <tr><th>Datetime</th><th>Author</th><th>Failure type</th><th>Note</th></tr>
@ -13,23 +13,28 @@
{{classification.who}} {{classification.who}}
</td> </td>
<td> <td>
<span th-star star-id="classification.failure_classification_id"></span> <span th-failure-classification failure-id="classification.failure_classification_id"></span>
</td> </td>
<td> <td>
{{classification.note}} {{classification.note}}
</td> </td>
<td>
<span ng-click="deleteClassification(classification)"
class="click-able-icon"
title="delete this classification">
<i class="fa fa-trash-o"></i>
</span>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div ng-show="classifications.length < 1"></br><em>This job has not been classified</em></div> <div ng-show="classifications.length < 1"></br><em>This job has not been classified</em></div>
</div> </div>
<div class="col-xs-1 bug-list-panel"> <div class="col-xs-2 bug-list-panel">
<h5><strong>Bugs</strong></h5> <h5><strong>Bugs</strong></h5>
<ul class="bug-list"> <ul class="bug-list">
<li ng-repeat="bug in bugs"> <li ng-repeat="bug in bugs">
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id={{bug.bug_id}}" <th-related-bug-saved></th-related-bug-saved>
target="_blank"
class="label label-default">{{bug.bug_id}}</a>
</li> </li>
</ul> </ul>
</div> </div>

Просмотреть файл

@ -1,90 +1,12 @@
"use strict"; "use strict";
treeherder.controller('BugClassificationCtrl',
function BugClassificationCtrl($scope, ThBugJobMapModel, $modalInstance){
$scope.failure_classification_id = null;
$scope.comment = "";
angular.forEach($scope.selected_bugs, function(bug_id, selected){
if(selected){
if($scope.comment !== ""){
$scope.comment += ",";
}
$scope.comment += "Bug #"+bug_id;
}
});
$scope.custom_bug = null;
$scope.ok = function () {
angular.forEach($scope.selected_bugs, function(bug_id, selected){
if(selected){
var bug_job_map = new ThBugJobMapModel({
bug_id: bug_id,
job_id: $scope.job.id
});
bug_job_map.create();
}
});
$modalInstance.close();
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
});
treeherder.controller('BugsPluginCtrl', treeherder.controller('BugsPluginCtrl',
function BugsPluginCtrl($scope, $rootScope, $log, ThJobArtifactModel, function BugsPluginCtrl($scope, $rootScope, ThLog, ThJobArtifactModel,
ThBugJobMapModel, ThJobClassificationModel, thNotify, $modal) { ThBugJobMapModel, ThJobClassificationModel, thNotify, $modal) {
var $log = new ThLog(this.constructor.name);
$log.debug("bugs plugin initialized"); $log.debug("bugs plugin initialized");
$scope.classify = function(bug_list){
var modalInstance = $modal.open({
templateUrl: 'bug_classification.html',
controller: 'BugClassificationCtrl',
resolve: {'result':'ok'},
scope: $scope
});
};
$scope.message = "";
$scope.quick_submit = function(){
angular.forEach($scope.selected_bugs, function(v, k){
if(v){
var bjm = new ThBugJobMapModel({
bug_id : k,
job_id: $scope.job.id,
type: 'annotation'
});
bjm.create();
}
});
var note = new ThJobClassificationModel({
job_id:$scope.job.id,
who: $scope.user ? $scope.user.email : "",
failure_classification_id: $scope.classification,
note_timestamp: new Date().getTime(),
note: ""
});
note.create()
.then(
function(){
thNotify.send({
message: "Note successfully created",
severity: "success",
sticky: false
});
},
function(){
thNotify.send({
message: "Note creation failed",
severity: "danger",
sticky: true
});
}
);
};
var update_bugs = function(newValue, oldValue){ var update_bugs = function(newValue, oldValue){
$scope.bugs = {}; $scope.bugs = {};
$scope.visible = "open"; $scope.visible = "open";

Просмотреть файл

@ -1,12 +1,12 @@
"use strict"; "use strict";
treeherder.controller('PluginCtrl', treeherder.controller('PluginCtrl',
function PluginCtrl($scope, $rootScope, thUrl, ThJobClassificationModel, function PluginCtrl($scope, $rootScope, thUrl, ThJobClassificationModel,
thClassificationTypes, ThJobModel, thEvents, dateFilter, thClassificationTypes, ThJobModel, thEvents, dateFilter,
numberFilter, ThBugJobMapModel, thResultStatus, thSocket, numberFilter, ThBugJobMapModel, thResultStatus, thSocket,
ThResultSetModel, $log) { ThResultSetModel, ThLog) {
var $log = new ThLog("PluginCtrl");
$scope.job = {}; $scope.job = {};
@ -18,7 +18,9 @@ treeherder.controller('PluginCtrl',
// get the details of the current job // get the details of the current job
ThJobModel.get($scope.job.id).then(function(data){ ThJobModel.get($scope.job.id).then(function(data){
_.extend($scope.job, data); $scope.job = data;
$scope.$broadcast(thEvents.jobDetailLoaded);
updateVisibleFields(); updateVisibleFields();
$scope.logs = data.logs; $scope.logs = data.logs;
}); });
@ -62,6 +64,17 @@ treeherder.controller('PluginCtrl',
}; };
}; };
/**
* Test whether or not the selected job is a reftest
*/
$scope.isReftest = function() {
if ($scope.selectedJob) {
return $scope.selectedJob.job_group_symbol === "R";
} else {
return false;
}
};
$rootScope.$on(thEvents.jobClick, function(event, job) { $rootScope.$on(thEvents.jobClick, function(event, job) {
selectJob(job, $rootScope.selectedJob); selectJob(job, $rootScope.selectedJob);
$rootScope.selectedJob = job; $rootScope.selectedJob = job;
@ -75,7 +88,7 @@ treeherder.controller('PluginCtrl',
$scope.updateBugs(); $scope.updateBugs();
}); });
$scope.classificationTypes = thClassificationTypes; $scope.classificationTypes = thClassificationTypes.classifications;
// load the list of existing classifications (including possibly a new one just // load the list of existing classifications (including possibly a new one just
// added). // added).
@ -100,7 +113,7 @@ treeherder.controller('PluginCtrl',
}; };
var updateClassification = function(classification){ var updateClassification = function(classification){
if(classification.who != $scope.user.email){ if(classification.who !== $scope.user.email){
// get a fresh version of the job // get a fresh version of the job
ThJobModel.get_list({id:classification.id}) ThJobModel.get_list({id:classification.id})
.then(function(job_list){ .then(function(job_list){
@ -123,7 +136,7 @@ treeherder.controller('PluginCtrl',
} }
} };
thSocket.on("job_classification", updateClassification); thSocket.on("job_classification", updateClassification);

Просмотреть файл

@ -1,11 +1,13 @@
"use strict"; "use strict";
treeherder.controller('PinboardCtrl', treeherder.controller('PinboardCtrl',
function PinboardCtrl($scope, $rootScope, thEvents, thPinboard, thNotify) { function PinboardCtrl($scope, $rootScope, thEvents, thPinboard, thNotify, ThLog) {
var $log = new ThLog(this.constructor.name);
$rootScope.$on(thEvents.jobPin, function(event, job) { $rootScope.$on(thEvents.jobPin, function(event, job) {
$scope.pinJob(job); $scope.pinJob(job);
$rootScope.$digest(); $scope.$digest();
}); });
$scope.pinJob = function(job) { $scope.pinJob = function(job) {
@ -44,6 +46,7 @@ treeherder.controller('PinboardCtrl',
} }
$scope.classification.who = $scope.user.email; $scope.classification.who = $scope.user.email;
thPinboard.save($scope.classification); thPinboard.save($scope.classification);
$rootScope.selectedJob = null;
} else { } else {
thNotify.send("must be logged in to classify jobs", "danger"); thNotify.send("must be logged in to classify jobs", "danger");
} }
@ -76,6 +79,7 @@ treeherder.controller('PinboardCtrl',
}; };
$scope.saveEnteredBugNumber = function() { $scope.saveEnteredBugNumber = function() {
$log.debug("new bug number to be saved: ", $scope.newEnteredBugNumber);
thPinboard.addBug({id:$scope.newEnteredBugNumber}); thPinboard.addBug({id:$scope.newEnteredBugNumber});
$scope.newEnteredBugNumber = null; $scope.newEnteredBugNumber = null;
$scope.toggleEnterBugNumber(); $scope.toggleEnterBugNumber();
@ -84,10 +88,12 @@ treeherder.controller('PinboardCtrl',
$scope.viewJob = function(job) { $scope.viewJob = function(job) {
$rootScope.selectedJob = job; $rootScope.selectedJob = job;
$rootScope.$broadcast(thEvents.jobClick, job); $rootScope.$broadcast(thEvents.jobClick, job);
$rootScope.$broadcast(thEvents.selectJob, job);
}; };
$scope.classification = thPinboard.createNewClassification(); $scope.classification = thPinboard.createNewClassification();
$scope.pinnedJobs = thPinboard.pinnedJobs; $scope.pinnedJobs = thPinboard.pinnedJobs;
$scope.relatedBugs = thPinboard.relatedBugs; $scope.relatedBugs = thPinboard.relatedBugs;
} }
); );

Просмотреть файл

@ -1,15 +1,19 @@
<div ng-controller="PluginCtrl"> <div ng-controller="PluginCtrl" class="full-height">
<div ng-controller="PinboardCtrl"> <div ng-controller="PinboardCtrl" class="full-height">
<div class="bottom-panel-title" ng-hide="hasPinnedJobs()">Pin one or more jobs to classify (shift-click).</div> <div class="bottom-panel-title" ng-hide="hasPinnedJobs()">Pin one or more jobs to classify (shift-click).</div>
<th-pinboard-panel></th-pinboard-panel>
<ng-include src="'partials/thPinboardPanel.html'"></ng-include>
<div id="bottom-left-panel" <div id="bottom-left-panel"
ng-class="{'with-pinboard-abs': hasPinnedJobs(), 'without-pinboard-abs': !hasPinnedJobs()}"> ng-class="{'with-pinboard-abs': hasPinnedJobs(), 'without-pinboard-abs': !hasPinnedJobs()}">
<div class="panel shadowed-panel"> <div class="panel shadowed-panel full-height">
<div class="panel-head"> <div class="panel-head">
<table class="table-super-condensed table-striped"> <table class="table-super-condensed table-striped {{resultStatusShading}}">
<tr> <tr>
<th class="small">Result</th> <th class="small">Result</th>
<td class="small {{ resultStatusClass }}">{{ job.result }}</td> <td class="small">{{ job.result }}</td>
</tr> </tr>
<tr> <tr>
<th class="small">Machine name</th> <th class="small">Machine name</th>
@ -22,9 +26,13 @@
</div> </div>
</div> </div>
</div> </div>
<div id="bottom-center-panel" <div id="bottom-center-panel"
class="full-height"
ng-class="{'with-pinboard-margin': hasPinnedJobs(), 'without-pinboard-margin': !hasPinnedJobs()}"> ng-class="{'with-pinboard-margin': hasPinnedJobs(), 'without-pinboard-margin': !hasPinnedJobs()}">
<div class="panel shadowed-panel bottom-shadowed-panel"> <div class="panel shadowed-panel"
ng-class="{'bottom-shadowed-panel-with-pinboard': hasPinnedJobs(), 'bottom-shadowed-panel-without-pinboard': !hasPinnedJobs()}">
<div class="panel-body resizable"> <div class="panel-body resizable">
<tabset class="tabs-below bottom-panel-tabs"> <tabset class="tabs-below bottom-panel-tabs">
<tab ng-repeat="(tab_id, tab) in tabs" active="tab.active" disabled="tab.disabled"> <tab ng-repeat="(tab_id, tab) in tabs" active="tab.active" disabled="tab.disabled">
@ -38,6 +46,8 @@
</div> </div>
</div> </div>
</div> </div>
<div id="bottom-menu" <div id="bottom-menu"
class="without-pinboard-abs"> class="without-pinboard-abs">
<div class="btn-group-vertical bottom-menu-group"> <div class="btn-group-vertical bottom-menu-group">
@ -65,7 +75,9 @@
ng-click="save()"> ng-click="save()">
<span class="glyphicon glyphicon-floppy-disk pull-left"></span> save <span class="glyphicon glyphicon-floppy-disk pull-left"></span> save
</button> </button>
<button type="button" class="btn btn-default btn-xs dropdown-toggle save-btn-dropdown" data-toggle="dropdown"> <button type="button"
class="btn btn-default btn-xs dropdown-toggle save-btn-dropdown"
data-toggle="dropdown">
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu pull-right" role="menu"> <ul class="dropdown-menu pull-right" role="menu">
@ -73,7 +85,11 @@
<li><a ng-click="saveBugsOnly()"><i class="fa fa-bug"></i> bugs only</a></li> <li><a ng-click="saveBugsOnly()"><i class="fa fa-bug"></i> bugs only</a></li>
</ul> </ul>
</div> </div>
<div class="btn-group-vertical"> <div class="btn-group-vertical bottom-menu-group">
<div class="btn btn-default btn-xs"
ng-show="isReftest()">
<a target="_blank" href="https://hg.mozilla.org/mozilla-central/raw-file/tip/layout/tools/reftest/reftest-analyzer.xhtml">Reftest Analyzer</a>
</div>
<div class="btn btn-default btn-xs" <div class="btn btn-default btn-xs"
ng-show="logs" ng-show="logs"
ng-disabled="artifacts.length>0"> ng-disabled="artifacts.length>0">

Просмотреть файл

@ -1,14 +1,16 @@
"use strict"; "use strict";
treeherder.controller('SimilarJobsPluginCtrl', treeherder.controller('SimilarJobsPluginCtrl',
function SimilarJobsPluginCtrl($scope, $log, ThJobModel, thResultStatusInfo) { function SimilarJobsPluginCtrl($scope, ThLog, $rootScope, ThJobModel, thResultStatusInfo, thEvents,
numberFilter, dateFilter, thClassificationTypes, thResultStatus,
ThJobArtifactModel) {
var $log = new ThLog(this.constructor.name);
$log.debug("similar jobs plugin initialized"); $log.debug("similar jobs plugin initialized");
// do the jobs retrieval based on the user selection
$scope.update_similar_jobs = function(newValue) { $scope.get_similar_jobs = function(){
if(newValue){$scope.similar_jobs_count = 20;} var options = {
if($scope.job.id){
var options = {
count: $scope.similar_jobs_count count: $scope.similar_jobs_count
}; };
angular.forEach($scope.similar_jobs_filters, function(value, key){ angular.forEach($scope.similar_jobs_filters, function(value, key){
@ -18,14 +20,25 @@ treeherder.controller('SimilarJobsPluginCtrl',
}); });
$log.log(options); $log.log(options);
ThJobModel.get_list(options).then(function(data){ ThJobModel.get_list(options).then(function(data){
$log.log(data);
$scope.similar_jobs = data; $scope.similar_jobs = data;
}); });
};
// reset the counter and retrieve the list of jobs
$scope.update_similar_jobs = function(event) {
if($scope.job){
$scope.similar_jobs_count = 20;
$scope.similar_job_selected = null;
}
if($scope.job.id){
$scope.get_similar_jobs();
} }
}; };
$scope.result_status_info = thResultStatusInfo; $scope.result_status_info = thResultStatusInfo;
$scope.$on(thEvents.jobDetailLoaded, $scope.update_similar_jobs);
$scope.$watch('job.job_guid', $scope.update_similar_jobs, true);
$scope.similar_jobs = []; $scope.similar_jobs = [];
$scope.similar_jobs_filters = { $scope.similar_jobs_filters = {
"machine_id": false, "machine_id": false,
@ -40,11 +53,48 @@ treeherder.controller('SimilarJobsPluginCtrl',
return thResultStatusInfo(resultState).btnClass; return thResultStatusInfo(resultState).btnClass;
}; };
// this is triggered by the show more link
$scope.show_more = function(){ $scope.show_more = function(){
$scope.similar_jobs_count += 20; $scope.similar_jobs_count += 20;
$scope.update_similar_jobs(); $scope.get_similar_jobs();
}; };
}
); $scope.similar_job_selected = null;
$scope.show_job_info = function(job){
ThJobModel.get(job.id)
.then(function(job){
$scope.similar_job_selected = job;
$scope.similar_job_selected.result_status = thResultStatus($scope.similar_job_selected);
var duration = (
$scope.similar_job_selected.end_timestamp - $scope.similar_job_selected.start_timestamp
)/60;
if (duration) {
duration = numberFilter(duration, 0) + " minutes";
}
$scope.similar_job_selected.duration = duration;
$scope.similar_job_selected.start_time = dateFilter(
$scope.similar_job_selected.start_timestamp*1000,
'short'
);
$scope.similar_job_selected.failure_classification_name = thClassificationTypes[
$scope.similar_job_selected.failure_classification_id
];
//retrieve the list of error lines
ThJobArtifactModel.get_list({
name: "Structured Log",
job_id: $scope.similar_job_selected.id
})
.then(function(artifact_list){
if(artifact_list.length > 0){
$scope.similar_job_selected.error_lines = artifact_list[0].blob.step_data.all_errors;
}
});
});
};
});

Просмотреть файл

@ -1,32 +1,75 @@
<div ng-controller="SimilarJobsPluginCtrl"> <div class="" ng-controller="SimilarJobsPluginCtrl">
<form role="form"> <div class="row">
<div class="checkbox"> <div class="col-xs-2">
<label> <form role="form">
<input ng-change="update_similar_jobs()" type="checkbox" ng-model="similar_jobs_filters.machine_id"/> <div class="checkbox">
Same machine: {{ job.machine_name }}
</label> <input ng-change="update_similar_jobs()" type="checkbox" ng-model="similar_jobs_filters.machine_id"/>
</div> <small>Same machine</small>
<div class="checkbox">
<label> </div>
<input ng-change="update_similar_jobs()" type="checkbox" ng-model="similar_jobs_filters.job_type_id"/> <div class="checkbox">
Same job type: {{ job.job_group_name }} {{ job.job_type_name }} ({{ job.job_type_symbol }})
</label> <input ng-change="update_similar_jobs()" type="checkbox" ng-model="similar_jobs_filters.job_type_id"/>
</div> <small>Same job type</small>
<div class="checkbox">
<label> </div>
<input ng-change="update_similar_jobs()" type="checkbox" ng-model="similar_jobs_filters.build_platform_id"/> <div class="checkbox">
Same platform: {{ job.platform }}
</label> <input ng-change="update_similar_jobs()" type="checkbox" ng-model="similar_jobs_filters.build_platform_id"/>
</div> <small>Same platform</small>
</form>
<p> </div>
<button class="btn btn-similar-jobs btn-xs" ng-class="button_class(job)" </form>
ng-repeat="job in similar_jobs | orderBy: -submit_timestamp"> </div>
{{job.job_type_symbol}} <div class="col-xs-10">
</button> <p class="similar_job_list">
<button ng-click="show_job_info(job)" class="btn btn-similar-jobs btn-xs" ng-class="button_class(job)"
ng-repeat="job in similar_jobs | orderBy: -submit_timestamp">
{{job.job_type_symbol}}
</button>
</p>
<a ng-click="show_more()">Show more</a>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<table class="table-bordered table-super-condensed" ng-if="similar_job_selected">
<tr>
<th>Job Name</th>
<th>Start time</th>
<th>Duration</th>
<th>Machine</th>
<th>Build</th>
<th>Result</th>
<th>Classification</th>
</tr>
<tr>
<td>{{ similar_job_selected.job_type_name }}</td>
<td>{{ similar_job_selected.start_time }}</td>
<td>{{ similar_job_selected.duration }}</td>
<td>{{ similar_job_selected.machine_platform_architecture }}
{{ similar_job_selected.machine_platform_os }}
</td>
<td>
{{ similar_job_selected.build_architecture }}
{{ similar_job_selected.build_platform }}
{{ similar_job_selected.build_os }}
</td>
<td>{{ similar_job_selected.result_status }}</td>
<td>{{ similar_job_selected.failure_classification_name }}</td>
</tr>
</table>
<ul class="list-unstyled" ng-if="similar_job_selected.error_lines.length > 0">
<li><h5>Error lines:</h5></li>
<li class="" ng-repeat="error in similar_job_selected.error_lines">
<small>{{error.line}}</small>
</li>
</ul>
</div>
</div>
</p>
<a ng-click="show_more()">Show more</a>
</div> </div>

Просмотреть файл

@ -1,7 +1,9 @@
"use strict"; "use strict";
treeherder.controller('TinderboxPluginCtrl', treeherder.controller('TinderboxPluginCtrl',
function TinderboxPluginCtrl($scope, $rootScope, $log, ThJobArtifactModel) { function TinderboxPluginCtrl($scope, $rootScope, ThLog, ThJobArtifactModel) {
var $log = new ThLog(this.constructor.name);
$log.debug("Tinderbox plugin initialized"); $log.debug("Tinderbox plugin initialized");
var update_job_info = function(newValue, oldValue){ var update_job_info = function(newValue, oldValue){
$scope.tinderbox_lines = []; $scope.tinderbox_lines = [];

Просмотреть файл

@ -3,17 +3,17 @@
<tr ng-repeat="line in tinderbox_lines_parsed"> <tr ng-repeat="line in tinderbox_lines_parsed">
<th>{{line.title}}</th> <th>{{line.title}}</th>
<td ng-switch on="line.type"> <td ng-switch on="line.type">
<a ng-switch-when="link" href="{{line.link}}">{{line.value}}</a> <a ng-switch-when="link" href="{{line.link}}" target="_blank">{{line.value}}</a>
<ul ng-switch-when="TalosResult"> <ul ng-switch-when="TalosResult">
<li>Datazilla: <li>Datazilla:
<ul> <ul>
<li ng-repeat="(k,v) in line.value.datazilla"><a href="{{v.url}}">{{k}}</a></li> <li ng-repeat="(k,v) in line.value.datazilla"><a href="{{v.url}}" target="_blank">{{k}}</a></li>
</ul> </ul>
</li> </li>
<li>Graphserver: <li>Graphserver:
<ul> <ul>
<li ng-repeat="(k,v) in line.value.graphserver">{{k}}:<a href="{{v.url}}">{{v.result}}</a></li> <li ng-repeat="(k,v) in line.value.graphserver">{{k}}:<a href="{{v.url}}" target="_blank">{{v.result}}</a></li>
</ul> </ul>
</li> </li>
</ul> </ul>