Bug 1434677 - Convert rendering list of pushes from angular to ReactJS (#3206)

This adds some new components and removes the AngularJS ng-repeat for
pushes.  In the course of this work, some of the AngularJS providers were
converted to helper functions.

In a couple cases, I had to add new code to the AngularJS areas so that it
would continue to interact well between Angular and React.

Also:
* Rename some functions and CSS classes from resultset to push
* Add unlistening for events during unmount of components
This commit is contained in:
Cameron Dawson 2018-02-20 10:31:11 -08:00 коммит произвёл GitHub
Родитель 7295cf324a
Коммит f984acf9a8
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
62 изменённых файлов: 1767 добавлений и 1583 удалений

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

@ -16,7 +16,7 @@ module.exports = neutrino => {
.loader('eslint', props => merge(props, {
options: {
plugins: ['react'],
envs: ['browser', 'es6', 'commonjs'],
envs: ['browser', 'es6', 'commonjs', 'jasmine'],
baseConfig: {
extends: ['airbnb']
},

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

@ -16,7 +16,7 @@ class TreeherderPage(Page):
_get_next_20_locator = (By.CSS_SELECTOR, 'div.btn:nth-child(2)')
_get_next_50_locator = (By.CSS_SELECTOR, 'div.btn:nth-child(3)')
_quick_filter_locator = (By.ID, 'quick-filter')
_result_sets_locator = (By.CSS_SELECTOR, '.result-set:not(.row)')
_pushes_locator = (By.CSS_SELECTOR, '.push:not(.row)')
_unchecked_repos_links_locator = (By.CSS_SELECTOR, '#repoLabel + .dropdown-menu .dropdown-checkbox:not([checked]) + .dropdown-link')
_unclassified_failure_count_locator = (By.ID, 'unclassified-failure-count')
_unclassified_failure_filter_locator = (By.CSS_SELECTOR, '.btn-unclassified-failures')
@ -31,12 +31,12 @@ class TreeherderPage(Page):
@property
def all_emails(self):
return list(itertools.chain.from_iterable(
r.emails for r in self.result_sets))
r.emails for r in self.pushes))
@property
def all_jobs(self):
return list(itertools.chain.from_iterable(
r.jobs for r in self.result_sets))
r.jobs for r in self.pushes))
@property
def info_panel(self):
@ -52,8 +52,8 @@ class TreeherderPage(Page):
return random_email_name.get_name
@property
def result_sets(self):
return [self.ResultSet(self, el) for el in self.find_elements(*self._result_sets_locator)]
def pushes(self):
return [self.ResultSet(self, el) for el in self.find_elements(*self._pushes_locator)]
@property
def search_term(self):
@ -92,7 +92,7 @@ class TreeherderPage(Page):
el = self.selenium.find_element(*self._quick_filter_locator)
el.send_keys(term)
el.send_keys(Keys.RETURN)
self.wait.until(lambda s: self.result_sets)
self.wait.until(lambda s: self.pushes)
elif method == 'keyboard':
self.find_element(By.CSS_SELECTOR, 'body').send_keys(
'f' + term + Keys.RETURN)
@ -103,10 +103,10 @@ class TreeherderPage(Page):
self.find_element(*self._unclassified_failure_filter_locator).click()
def _get_next(self, count):
before = len(self.result_sets)
before = len(self.pushes)
locator = getattr(self, '_get_next_{}_locator'.format(count))
self.find_element(*locator).click()
self.wait.until(lambda s: len(self.result_sets) == before + count)
self.wait.until(lambda s: len(self.pushes) == before + count)
def get_next_10(self):
self._get_next(10)
@ -145,8 +145,8 @@ class TreeherderPage(Page):
class ResultSet(Region):
_busted_jobs_locator = (By.CSS_SELECTOR, '.job-btn.filter-shown.btn-red')
_datestamp_locator = (By.CSS_SELECTOR, '.result-set-title-left > span a')
_email_locator = (By.CSS_SELECTOR, '.result-set-title-left > th-author > span > a')
_datestamp_locator = (By.CSS_SELECTOR, '.push-title-left > span a')
_email_locator = (By.CSS_SELECTOR, '.push-title-left > .push-author > span > a')
_exception_jobs_locator = (By.CSS_SELECTOR, '.job-btn.filter-shown.btn-purple')
_job_groups_locator = (By.CSS_SELECTOR, '.job-group')
_jobs_locator = (By.CSS_SELECTOR, '.job-btn.filter-shown')

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

@ -9,7 +9,7 @@ from pages.treeherder import TreeherderPage
def test_expanding_group_count(base_url, selenium):
page = TreeherderPage(selenium, base_url).open()
all_groups = list(itertools.chain.from_iterable(
r.job_groups for r in page.result_sets))
r.job_groups for r in page.pushes))
group = next(g for g in all_groups if not g.expanded)
jobs = group.jobs
group.expand()

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

@ -10,7 +10,7 @@ def test_filter_by_email(base_url, selenium):
page = TreeherderPage(selenium, base_url).open()
page.select_random_email()
filtered_emails_name = page.result_sets[0].email_name
filtered_emails_name = page.pushes[0].email_name
random_email_name = page.random_email_name
assert filtered_emails_name == random_email_name

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

@ -7,9 +7,9 @@ from pages.treeherder import TreeherderPage
def test_filter_jobs(base_url, selenium):
"""Open resultset page and filter for platform"""
page = TreeherderPage(selenium, base_url).open()
assert any(r.contains_platform('linux') for r in page.result_sets)
assert any(r.contains_platform('windows') for r in page.result_sets)
assert any(r.contains_platform('linux') for r in page.pushes)
assert any(r.contains_platform('windows') for r in page.pushes)
page.filter_by('linux')
assert any(r.contains_platform('linux') for r in page.result_sets)
assert not any(r.contains_platform('windows') for r in page.result_sets)
assert any(r.contains_platform('linux') for r in page.pushes)
assert not any(r.contains_platform('windows') for r in page.pushes)

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

@ -7,6 +7,6 @@ from pages.treeherder import TreeherderPage
@pytest.mark.parametrize('count', ((10), (20), (50)))
def test_get_next_results(base_url, selenium, count):
page = TreeherderPage(selenium, base_url).open()
assert len(page.result_sets) == 10
assert len(page.pushes) == 10
getattr(page, 'get_next_{}'.format(count))()
assert len(page.result_sets) == 10 + count
assert len(page.pushes) == 10 + count

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

@ -6,14 +6,14 @@ from pages.treeherder import TreeherderPage
@pytest.mark.nondestructive
def test_load_next_results(base_url, selenium):
page = TreeherderPage(selenium, base_url).open()
assert len(page.result_sets) == 10
assert len(page.pushes) == 10
page.get_next_10()
assert len(page.result_sets) == 20
assert len(page.pushes) == 20
page.get_next_20()
page.wait_for_page_to_load()
assert len(page.result_sets) == 40
assert len(page.pushes) == 40
page.get_next_50()
assert len(page.result_sets) == 90
assert len(page.pushes) == 90

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

@ -39,6 +39,6 @@ def test_clear_pinboard(base_url, selenium):
def test_pin_all_jobs(base_url, selenium):
"""Open treeherder page, pin all jobs, confirm no more than 500 pins in pinboard"""
page = TreeherderPage(selenium, base_url).open()
result_set = next(r for r in page.result_sets if len(r.jobs) > 1)
result_set = next(r for r in page.pushes if len(r.jobs) > 1)
result_set.pin_all_jobs()
assert len(result_set.jobs) <= len(page.pinboard.jobs) <= 500

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

@ -24,7 +24,7 @@ class Treeherder(Base):
_quick_filter_locator = (By.ID, 'quick-filter')
_repo_locator = (By.CSS_SELECTOR, '#repo-dropdown a[href*="repo={}"]')
_repo_menu_locator = (By.ID, 'repoLabel')
_result_sets_locator = (By.CSS_SELECTOR, '.result-set:not(.row)')
_pushes_locator = (By.CSS_SELECTOR, '.push:not(.row)')
_watched_repos_locator = (By.CSS_SELECTOR, '#watched-repo-navbar th-watched-repo')
def wait_for_page_to_load(self):
@ -38,7 +38,7 @@ class Treeherder(Base):
@property
def all_jobs(self):
return list(itertools.chain.from_iterable(
r.jobs for r in self.result_sets))
r.jobs for r in self.pushes))
@contextmanager
def filters_menu(self):
@ -52,8 +52,8 @@ class Treeherder(Base):
return self.InfoPanel(self)
@property
def result_sets(self):
return [self.ResultSet(self, el) for el in self.find_elements(*self._result_sets_locator)]
def pushes(self):
return [self.ResultSet(self, el) for el in self.find_elements(*self._pushes_locator)]
@property
def quick_filter_term(self):
@ -130,13 +130,13 @@ class Treeherder(Base):
class ResultSet(Region):
_author_locator = (By.CSS_SELECTOR, '.result-set-title-left th-author a')
_datestamp_locator = (By.CSS_SELECTOR, '.result-set-title-left > span a')
_author_locator = (By.CSS_SELECTOR, '.push-title-left .push-author a')
_datestamp_locator = (By.CSS_SELECTOR, '.push-title-left > span a')
_dropdown_toggle_locator = (By.CLASS_NAME, 'dropdown-toggle')
_commits_locator = (By.CSS_SELECTOR, '.revision-list .revision')
_jobs_locator = (By.CSS_SELECTOR, '.job-btn.filter-shown')
_set_bottom_of_range_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu > li:nth-child(9) > a')
_set_top_of_range_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu > li:nth-child(8) > a')
_set_bottom_of_range_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu > li:nth-child(6)')
_set_top_of_range_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu > li:nth-child(5)')
@property
def author(self):

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

@ -3,17 +3,17 @@ from pages.treeherder import Treeherder
def test_set_as_top_of_range(base_url, selenium, test_job):
page = Treeherder(selenium, base_url).open()
result_sets = page.result_sets
datestamp = result_sets[1].datestamp
assert result_sets[0].datestamp != datestamp
result_sets[1].set_as_top_of_range()
assert page.result_sets[0].datestamp == datestamp
pushes = page.pushes
datestamp = pushes[1].datestamp
assert pushes[0].datestamp != datestamp
pushes[1].set_as_top_of_range()
assert page.pushes[0].datestamp == datestamp
def test_set_as_bottom_of_range(base_url, selenium, test_job):
page = Treeherder(selenium, base_url).open()
result_sets = page.result_sets
datestamp = result_sets[-2].datestamp
assert result_sets[-1].datestamp != datestamp
page.result_sets[-2].set_as_bottom_of_range()
assert page.result_sets[-1].datestamp == datestamp
pushes = page.pushes
datestamp = pushes[-2].datestamp
assert pushes[-1].datestamp != datestamp
page.pushes[-2].set_as_bottom_of_range()
assert page.pushes[-1].datestamp == datestamp

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

@ -3,11 +3,11 @@ from pages.treeherder import Treeherder
def test_open_single_result(base_url, selenium, test_commit):
page = Treeherder(selenium, base_url).open()
page.wait.until(lambda _: 1 == len(page.result_sets))
page.result_sets[0].view()
assert 1 == len(page.result_sets)
assert test_commit.author == page.result_sets[0].author
assert test_commit.push.time.strftime('%a %b %-d, %H:%M:%S') == page.result_sets[0].datestamp
assert 1 == len(page.result_sets[0].commits)
assert test_commit.revision[:12] == page.result_sets[0].commits[0].revision
assert test_commit.comments == page.result_sets[0].commits[0].comment
page.wait.until(lambda _: 1 == len(page.pushes))
page.pushes[0].view()
assert 1 == len(page.pushes)
assert test_commit.author == page.pushes[0].author
assert test_commit.push.time.strftime('%a, %b %-d, %H:%M:%S') == page.pushes[0].datestamp
assert 1 == len(page.pushes[0].commits)
assert test_commit.revision[:12] == page.pushes[0].commits[0].revision
assert test_commit.comments == page.pushes[0].commits[0].comment

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

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

@ -1,129 +0,0 @@
/* jasmine specs for controllers go here */
describe('JobsCtrl', function(){
var $httpBackend, controller, jobsScope;
beforeEach(angular.mock.module('treeherder.app'));
beforeEach(inject(function ($injector, $rootScope, $controller) {
var activeRepo = 'mozilla-central';
var projectPrefix = '/api/project/' + activeRepo + '/';
$httpBackend = $injector.get('$httpBackend');
jasmine.getJSONFixtures().fixturesPath='base/tests/ui/mock';
$httpBackend.whenGET('/api/repository/').respond(
getJSONFixture('repositories.json')
);
$httpBackend.whenGET(projectPrefix + 'resultset/?count=10&full=true').respond(
getJSONFixture('resultset_list.json')
);
$httpBackend.whenGET(projectPrefix + 'jobs/?count=2000&result_set_id=1&return_type=list').respond(
getJSONFixture('job_list/job_1.json')
);
$httpBackend.whenGET(projectPrefix + 'jobs/?count=2000&result_set_id=2&return_type=list').respond(
getJSONFixture('job_list/job_2.json')
);
$httpBackend.whenGET('https://treestatus.mozilla-releng.net/trees/mozilla-central').respond(
{
"result": {
"status": "closed",
"message_of_the_day": "See the <a href=\"https://wiki.mozilla.org/Tree_Rules/Inbound\">Inbound tree rules</a> before pushing. <a href=\"https://sheriffs.etherpad.mozilla.org/sheriffing-notes\">Sheriff notes/current issues</a>.",
"tree": "mozilla-central",
"reason": "Bustage"
}
}
);
$httpBackend.whenGET('/api/project/mozilla-central/jobs/0/unclassified_failure_count/').respond(
{
"unclassified_failure_count": 1152,
"repository": "mozilla-central"
}
);
$httpBackend.whenGET('/api/jobtype/').respond(
getJSONFixture('job_type_list.json')
);
$httpBackend.whenGET('/api/jobgroup/').respond(
getJSONFixture('job_group_list.json')
);
jobsScope = $rootScope.$new();
jobsScope.repoName = activeRepo;
jobsScope.setRepoPanelShowing = function(tf) {
// no op in the tests.
};
$controller('JobsCtrl', {'$scope': jobsScope});
}));
/*
Tests JobsCtrl
*/
it('should have 2 resultsets', function() {
$httpBackend.flush();
expect(jobsScope.result_sets).toBeDefined();
expect(jobsScope.result_sets.length).toBe(2);
});
it('should have a job_map', function(){
$httpBackend.flush();
expect(jobsScope.job_map).toBeDefined();
});
it('should know when it\'s loading the initial data', function() {
expect(jobsScope.isLoadingRsBatch).toEqual(
{appending: true, prepending: false }
);
$httpBackend.flush();
expect(jobsScope.isLoadingRsBatch).toEqual(
{appending: false, prepending: false }
);
});
it('should have 2 platforms in resultset 1', function() {
// because some http requests are deferred to after first
// ingestion, we need to flush twice
$httpBackend.flush();
$httpBackend.flush();
expect(jobsScope.result_sets[0].platforms.length).toBe(2);
});
/*
Tests ResultSetCtrl
*/
describe('ResultSetCtrl', function(){
var resultSetScopeList;
beforeEach(inject(function ($rootScope, $controller) {
/*
* ResultSetCtrl is created insided a ng-repeat
* so we should have a list of resultSetscope
*/
// because some http requests are deferred to after first
// ingestion, we need to flush twice
$httpBackend.flush();
$httpBackend.flush();
resultSetScopeList = [];
for(var i=0; i<jobsScope.result_sets.length; i++){
var resultSetScope = jobsScope.$new();
resultSetScope.resultset = jobsScope.result_sets[i];
$controller('ResultSetCtrl', {'$scope': resultSetScope});
resultSetScopeList.push(resultSetScope);
}
}));
it('should set the selectedJob in scope when calling viewJob()', function() {
var job = resultSetScopeList[0].resultset.platforms[0].groups[0].jobs[0];
var resultSetScope = resultSetScopeList[0];
resultSetScope.viewJob(job);
expect(resultSetScope.selectedJob).toBe(job);
});
});
});

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

@ -38,10 +38,10 @@ describe('ThResultSetStore', function(){
);
$httpBackend.whenGET(foregroundPrefix + '/resultset/?count=10&full=true').respond(
getJSONFixture('resultset_list.json')
getJSONFixture('push_list.json')
);
$httpBackend.whenGET(foregroundPrefix + '/jobs/?count=2000&result_set_id=1&return_type=list').respond(
getJSONFixture('job_list/job_1.json')
);

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

@ -0,0 +1,145 @@
import React from 'react';
import { mount } from 'enzyme';
import PushList from '../../../../ui/job-view/PushList';
import Push from '../../../../ui/job-view/Push';
import PushJobs from '../../../../ui/job-view/PushJobs';
import Platform from '../../../../ui/job-view/Platform';
import JobButton from '../../../../ui/job-view/JobButton';
describe('PushList component', () => {
let $injector, $httpBackend, pushListEl, pushList;
const repoName = 'mozilla-central';
beforeEach(angular.mock.module('treeherder'));
beforeEach(inject((_$injector_) => {
const projectPrefix = '/api/project/' + repoName + '/';
$injector = _$injector_;
// TODO: Once we switch away from angular for fetching data, we may want to
// switch to using something like this for mocking test data: http://www.wheresrhys.co.uk/fetch-mock/
$httpBackend = $injector.get('$httpBackend');
const ThResultSetStore = $injector.get('ThResultSetStore');
jasmine.getJSONFixtures().fixturesPath = 'base/tests/ui/mock';
$httpBackend.whenGET('/api/repository/').respond(
getJSONFixture('repositories.json')
);
$httpBackend.whenGET(projectPrefix + 'resultset/?count=10&full=true').respond(
getJSONFixture('push_list.json')
);
$httpBackend.whenGET(projectPrefix + 'resultset/?count=11&full=true&push_timestamp__lte=1424272126').respond(
getJSONFixture('push_list.json')
);
$httpBackend.whenGET(projectPrefix + 'jobs/?count=2000&result_set_id=1&return_type=list').respond(
getJSONFixture('job_list/job_1.json')
);
$httpBackend.whenGET(projectPrefix + 'jobs/?count=2000&result_set_id=2&return_type=list').respond(
getJSONFixture('job_list/job_2.json')
);
$httpBackend.whenGET('https://treestatus.mozilla-releng.net/trees/mozilla-central').respond(
{
result: {
status: "closed",
message_of_the_day: "This is a message nstuff...",
tree: "mozilla-central",
reason: "Bustage"
}
}
);
ThResultSetStore.addRepository(repoName);
ThResultSetStore.fetchResultSets(repoName, 10);
$httpBackend.flush();
pushList = ThResultSetStore.getResultSetsArray(repoName);
}));
/*
Tests Jobs view
*/
it('should have 2 Pushes', () => {
pushListEl = mount(
<PushList
$injector={$injector}
repoName={repoName}
user={{}}
currentRepo={{ name: "mozilla-inbound", url: "http://foo.baz" }}
/>
);
$httpBackend.flush();
pushListEl.setState({ pushList });
expect(pushListEl.find(Push).length).toEqual(2);
});
});
describe('PushJobs component', () => {
let $injector, pushJobsEl, pushList, ThResultSetStore;
const repoName = 'mozilla-central';
const projectPrefix = '/api/project/' + repoName + '/';
beforeEach(angular.mock.module('treeherder'));
beforeEach(inject((_$injector_) => {
$injector = _$injector_;
jasmine.getJSONFixtures().fixturesPath = 'base/tests/ui/mock';
const $httpBackend = $injector.get('$httpBackend');
ThResultSetStore = $injector.get('ThResultSetStore');
jasmine.getJSONFixtures().fixturesPath = 'base/tests/ui/mock';
$httpBackend.whenGET('/api/repository/').respond(
getJSONFixture('repositories.json')
);
$httpBackend.whenGET(projectPrefix + 'resultset/?count=10&full=true').respond(
getJSONFixture('push_list.json')
);
$httpBackend.whenGET(projectPrefix + 'jobs/?count=2000&result_set_id=1&return_type=list').respond(
getJSONFixture('job_list/job_1.json')
);
$httpBackend.whenGET(projectPrefix + 'jobs/?count=2000&result_set_id=2&return_type=list').respond(
getJSONFixture('job_list/job_2.json')
);
$httpBackend.whenGET('https://treestatus.mozilla-releng.net/trees/mozilla-central').respond(
{
result: {
status: "closed",
message_of_the_day: "This is a message nstuff...",
tree: "mozilla-central",
reason: "Bustage"
}
}
);
ThResultSetStore.addRepository(repoName);
ThResultSetStore.fetchResultSets(repoName, 10);
$httpBackend.flush();
pushList = ThResultSetStore.getResultSetsArray(repoName);
pushJobsEl = mount(
<PushJobs
$injector={$injector}
push={pushList[1]}
repoName={repoName}
/>
);
}));
it('should have platforms', () => {
expect(pushJobsEl.find(Platform).length).toEqual(2);
});
it('should set the selected job when calling selectJob()', () => {
const jobEl = pushJobsEl.find(JobButton).first();
const jobInst = jobEl.instance();
jobInst.ThResultSetStore = ThResultSetStore;
jobEl.simulate('mouseDown');
expect(jobInst.state.isSelected).toEqual(true);
});
});

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

@ -1,58 +1,61 @@
import { mount } from 'enzyme';
import { Revision, Initials } from '../../../../ui/job-view/Revision';
import { RevisionList, MoreRevisionsLink } from '../../../../ui/job-view/RevisionList';
import {
RevisionList,
MoreRevisionsLink
} from '../../../../ui/job-view/RevisionList';
describe('Revision list component', () => {
let $injector, mockData;
beforeEach(angular.mock.module('treeherder'));
beforeEach(inject((_$injector_) => {
$injector = _$injector_;
let $injector, mockData;
beforeEach(angular.mock.module('treeherder'));
beforeEach(inject((_$injector_) => {
$injector = _$injector_;
const repo = {
"id": 2,
"repository_group": {
"name": "development",
"description": ""
},
"name": "mozilla-inbound",
"dvcs_type": "hg",
"url": "https://hg.mozilla.org/integration/mozilla-inbound",
"branch": null,
"codebase": "gecko",
"description": "",
"active_status": "active",
"performance_alerts_enabled": true,
"pushlogURL": "https://hg.mozilla.org/integration/mozilla-inbound/pushloghtml"
};
// Mock these simple functions so we don't have to call ThRepositoryModel.load() first to use them
repo.getRevisionHref = () => `${repo.url}/rev/${push.revision}`;
repo.getPushLogHref = (revision) => `${repo.pushlogURL}?changeset=${revision}`;
const repo = {
id: 2,
repository_group: {
name: "development",
description: ""
},
name: "mozilla-inbound",
dvcs_type: "hg",
url: "https://hg.mozilla.org/integration/mozilla-inbound",
branch: null,
codebase: "gecko",
description: "",
active_status: "active",
performance_alerts_enabled: true,
pushlogURL: "https://hg.mozilla.org/integration/mozilla-inbound/pushloghtml"
};
// Mock these simple functions so we don't have to call ThRepositoryModel.load() first to use them
repo.getRevisionHref = () => `${repo.url}/rev/${push.revision}`;
repo.getPushLogHref = revision => `${repo.pushlogURL}?changeset=${revision}`;
const push = {
"id": 151371,
"revision": "5a110ad242ead60e71d2186bae78b1fb766ad5ff",
"revision_count": 3,
"author": "ryanvm@gmail.com",
"push_timestamp": 1481326280,
"repository_id": 2,
"revisions": [{
"result_set_id": 151371,
"repository_id": 2,
"revision": "5a110ad242ead60e71d2186bae78b1fb766ad5ff",
"author": "André Bargull <andre.bargull@gmail.com>",
"comments": "Bug 1319926 - Part 2: Collect telemetry about deprecated String generics methods. r=jandem"
id: 151371,
revision: "5a110ad242ead60e71d2186bae78b1fb766ad5ff",
revision_count: 3,
author: "ryanvm@gmail.com",
push_timestamp: 1481326280,
repository_id: 2,
revisions: [{
result_set_id: 151371,
repository_id: 2,
revision: "5a110ad242ead60e71d2186bae78b1fb766ad5ff",
author: "André Bargull <andre.bargull@gmail.com>",
comments: "Bug 1319926 - Part 2: Collect telemetry about deprecated String generics methods. r=jandem"
}, {
"result_set_id": 151371,
"repository_id": 2,
"revision": "07d6bf74b7a2552da91b5e2fce0fa0bc3b457394",
"author": "André Bargull <andre.bargull@gmail.com>",
"comments": "Bug 1319926 - Part 1: Warn when deprecated String generics methods are used. r=jandem"
result_set_id: 151371,
repository_id: 2,
revision: "07d6bf74b7a2552da91b5e2fce0fa0bc3b457394",
author: "André Bargull <andre.bargull@gmail.com>",
comments: "Bug 1319926 - Part 1: Warn when deprecated String generics methods are used. r=jandem"
}, {
"result_set_id": 151371,
"repository_id": 2,
"revision": "e83eaf2380c65400dc03c6f3615d4b2cef669af3",
"author": "Frédéric Wang <fred.wang@free.fr>",
"comments": "Bug 1322743 - Add STIX Two Math to the list of math fonts. r=karlt"
result_set_id: 151371,
repository_id: 2,
revision: "e83eaf2380c65400dc03c6f3615d4b2cef669af3",
author: "Frédéric Wang <fred.wang@free.fr>",
comments: "Bug 1322743 - Add STIX Two Math to the list of math fonts. r=karlt"
}]
};
mockData = {
@ -61,142 +64,168 @@ describe('Revision list component', () => {
};
}));
it('renders the correct number of revisions in a list', () => {
const wrapper = mount(<RevisionList repo={mockData.repo} push={mockData.push}
$injector={$injector}/>);
expect(wrapper.find(Revision).length).toEqual(mockData['push']['revision_count']);
});
it('renders the correct number of revisions in a list', () => {
const wrapper = mount(
<RevisionList
repo={mockData.repo} push={mockData.push}
$injector={$injector}
/>
);
expect(wrapper.find(Revision).length).toEqual(mockData.push.revision_count);
});
it('renders an "...and more" link if the revision count is higher than the max display count of 20', () => {
mockData.push.revision_count = 21;
it('renders an "...and more" link if the revision count is higher than the max display count of 20', () => {
mockData.push.revision_count = 21;
const wrapper = mount(<RevisionList repo={mockData.repo} push={mockData.push}
$injector={$injector}/>);
expect(wrapper.find(MoreRevisionsLink).length).toEqual(1);
});
const wrapper = mount(
<RevisionList
repo={mockData.repo} push={mockData.push}
$injector={$injector}
/>
);
expect(wrapper.find(MoreRevisionsLink).length).toEqual(1);
});
});
describe('Revision item component', () => {
let $injector, linkifyBugsFilter, mockData;
beforeEach(angular.mock.module('treeherder'));
beforeEach(inject((_$injector_, $filter) => {
$injector = _$injector_;
linkifyBugsFilter = $filter('linkifyBugs');
let linkifyBugsFilter, mockData;
beforeEach(angular.mock.module('treeherder'));
beforeEach(inject((_$injector_, $filter) => {
linkifyBugsFilter = $filter('linkifyBugs');
const repo = {
"id": 2,
"repository_group": {
"name": "development",
"description": ""
},
"name": "mozilla-inbound",
"dvcs_type": "hg",
"url": "https://hg.mozilla.org/integration/mozilla-inbound",
"branch": null,
"codebase": "gecko",
"description": "",
"active_status": "active",
"performance_alerts_enabled": true,
"pushlogURL": "https://hg.mozilla.org/integration/mozilla-inbound/pushloghtml"
};
const revision = {
"result_set_id": 151371,
"repository_id": 2,
"revision": "5a110ad242ead60e71d2186bae78b1fb766ad5ff",
"author": "André Bargull <andre.bargull@gmail.com>",
"comments": "Bug 1319926 - Part 2: Collect telemetry about deprecated String generics methods. r=jandem"
};
// Mock these simple functions so we don't have to call ThRepositoryModel.load() first to use them
repo.getRevisionHref = () => `${repo.url}/rev/${revision.revision}`;
repo.getPushLogHref = (revision) => `${repo.pushlogURL}?changeset=${revision}`;
const repo = {
id: 2,
repository_group: {
name: "development",
description: ""
},
name: "mozilla-inbound",
dvcs_type: "hg",
url: "https://hg.mozilla.org/integration/mozilla-inbound",
branch: null,
codebase: "gecko",
description: "",
active_status: "active",
performance_alerts_enabled: true,
pushlogURL: "https://hg.mozilla.org/integration/mozilla-inbound/pushloghtml"
};
const revision = {
result_set_id: 151371,
repository_id: 2,
revision: "5a110ad242ead60e71d2186bae78b1fb766ad5ff",
author: "André Bargull <andre.bargull@gmail.com>",
comments: "Bug 1319926 - Part 2: Collect telemetry about deprecated String generics methods. r=jandem"
};
// Mock these simple functions so we don't have to call ThRepositoryModel.load() first to use them
repo.getRevisionHref = () => `${repo.url}/rev/${revision.revision}`;
repo.getPushLogHref = revision => `${repo.pushlogURL}?changeset=${revision}`;
mockData = {
revision,
repo
};
}));
mockData = {
revision,
repo
};
}));
it('renders a linked revision', () => {
const wrapper = mount(<Revision repo={mockData.repo} revision={mockData.revision}
linkifyBugsFilter={linkifyBugsFilter}/>);
const link = wrapper.find('a');
expect(link.length).toEqual(1);
expect(link.props().href).toEqual(mockData.repo.getRevisionHref());
expect(link.props().title).toEqual(`Open revision ${mockData.revision.revision} on ${mockData.repo.url}`);
});
it('renders a linked revision', () => {
const wrapper = mount(
<Revision
repo={mockData.repo}
revision={mockData.revision}
linkifyBugsFilter={linkifyBugsFilter}
/>);
const link = wrapper.find('a');
expect(link.length).toEqual(1);
expect(link.props().href).toEqual(mockData.repo.getRevisionHref());
expect(link.props().title).toEqual(`Open revision ${mockData.revision.revision} on ${mockData.repo.url}`);
});
it(`renders the contributors' initials`, () => {
const wrapper = mount(<Revision repo={mockData.repo} revision={mockData.revision}
linkifyBugsFilter={linkifyBugsFilter}/>);
const initials = wrapper.find('.user-push-initials');
expect(initials.length).toEqual(1);
expect(initials.text()).toEqual('AB');
});
it(`renders the contributors' initials`, () => {
const wrapper = mount(
<Revision
repo={mockData.repo}
revision={mockData.revision}
linkifyBugsFilter={linkifyBugsFilter}
/>);
const initials = wrapper.find('.user-push-initials');
expect(initials.length).toEqual(1);
expect(initials.text()).toEqual('AB');
});
it('linkifies bug IDs in the comments', () => {
const wrapper = mount(<Revision repo={mockData.repo} revision={mockData.revision}
linkifyBugsFilter={linkifyBugsFilter}/>);
const escapedComment = _.escape(mockData.revision.comments.split('\n')[0]);
const linkifiedCommentText = linkifyBugsFilter(escapedComment);
it('linkifies bug IDs in the comments', () => {
const wrapper = mount(
<Revision
repo={mockData.repo}
revision={mockData.revision}
linkifyBugsFilter={linkifyBugsFilter}
/>);
const escapedComment = _.escape(mockData.revision.comments.split('\n')[0]);
const linkifiedCommentText = linkifyBugsFilter(escapedComment);
const comment = wrapper.find('.revision-comment em');
expect(comment.html()).toEqual(`<em>${linkifiedCommentText}</em>`);
});
const comment = wrapper.find('.revision-comment em');
expect(comment.html()).toEqual(`<em>${linkifiedCommentText}</em>`);
});
it('marks the revision as backed out if the words "Back/Backed out" appear in the comments', () => {
mockData.revision.comments = "Backed out changeset a6e2d96c1274 (bug 1322565) for eslint failure";
let wrapper = mount(<Revision repo={mockData.repo} revision={mockData.revision}
linkifyBugsFilter={linkifyBugsFilter}/>);
expect(wrapper.find({'data-tags': 'backout'}).length).toEqual(1);
it('marks the revision as backed out if the words "Back/Backed out" appear in the comments', () => {
mockData.revision.comments = "Backed out changeset a6e2d96c1274 (bug 1322565) for eslint failure";
let wrapper = mount(
<Revision
repo={mockData.repo}
revision={mockData.revision}
linkifyBugsFilter={linkifyBugsFilter}
/>);
expect(wrapper.find({ 'data-tags': 'backout' }).length).toEqual(1);
mockData.revision.comments = "Back out changeset a6e2d96c1274 (bug 1322565) for eslint failure";
wrapper = mount(<Revision repo={mockData.repo} revision={mockData.revision}
linkifyBugsFilter={linkifyBugsFilter}/>);
expect(wrapper.find({'data-tags': 'backout'}).length).toEqual(1);
});
mockData.revision.comments = "Back out changeset a6e2d96c1274 (bug 1322565) for eslint failure";
wrapper = mount(
<Revision
repo={mockData.repo} revision={mockData.revision}
linkifyBugsFilter={linkifyBugsFilter}
/>);
expect(wrapper.find({ 'data-tags': 'backout' }).length).toEqual(1);
});
});
describe('More revisions link component', () => {
it('renders an "...and more" link', () => {
const wrapper = mount(<MoreRevisionsLink href='http://more.link/'/>);
const link = wrapper.find('a');
expect(link.props().href).toEqual('http://more.link/');
expect(link.text()).toEqual('\u2026and more');
});
it('renders an "...and more" link', () => {
const wrapper = mount(<MoreRevisionsLink href="http://more.link/" />);
const link = wrapper.find('a');
expect(link.props().href).toEqual('http://more.link/');
expect(link.text()).toEqual('\u2026and more');
});
it('has an external link icon', () => {
const wrapper = mount(<MoreRevisionsLink href='http://more.link'/>);
expect(wrapper.find('i.fa.fa-external-link-square').length).toEqual(1);
});
it('has an external link icon', () => {
const wrapper = mount(<MoreRevisionsLink href="http://more.link" />);
expect(wrapper.find('i.fa.fa-external-link-square').length).toEqual(1);
});
});
describe('initials filter', function() {
const email = "foo@bar.baz";
it('initializes a one-word name', function() {
const name = 'Starscream';
const initials = mount(<Initials title={`${name}: ${email}`}
author={name}
/>);
expect(initials.html()).toEqual('<span><span class="user-push-icon" title="Starscream: foo@bar.baz"><i class="fa fa-user-o" aria-hidden="true"></i></span><div class="icon-superscript user-push-initials">S</div></span>');
});
describe('initials filter', function () {
const email = "foo@bar.baz";
it('initializes a one-word name', function () {
const name = 'Starscream';
const initials = mount(<Initials title={`${name}: ${email}`}
author={name}
/>);
expect(initials.html()).toEqual('<span><span class="user-push-icon" title="Starscream: foo@bar.baz"><i class="fa fa-user-o" aria-hidden="true"></i></span><div class="icon-superscript user-push-initials">S</div></span>');
});
it('initializes a two-word name', function() {
const name = 'Optimus Prime';
const initials = mount(<Initials title={`${name}: ${email}`}
author={name}
/>);
const userPushInitials = initials.find('.user-push-initials');
expect(userPushInitials.html()).toEqual('<div class="icon-superscript user-push-initials">OP</div>');
});
it('initializes a two-word name', function () {
const name = 'Optimus Prime';
const initials = mount(<Initials title={`${name}: ${email}`}
author={name}
/>);
const userPushInitials = initials.find('.user-push-initials');
expect(userPushInitials.html()).toEqual('<div class="icon-superscript user-push-initials">OP</div>');
});
it('initializes a three-word name', function() {
const name = 'Some Other Transformer';
const initials = mount(<Initials title={`${name}: ${email}`}
author={name}
/>);
const userPushInitials = initials.find('.user-push-initials');
expect(userPushInitials.html()).toEqual('<div class="icon-superscript user-push-initials">ST</div>');
});
it('initializes a three-word name', function () {
const name = 'Some Other Transformer';
const initials = mount(<Initials title={`${name}: ${email}`}
author={name}
/>);
const userPushInitials = initials.find('.user-push-initials');
expect(userPushInitials.html()).toEqual('<div class="icon-superscript user-push-initials">ST</div>');
});
});

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

@ -85,6 +85,10 @@ input[type="checkbox"] {
cursor: not-allowed;
}
.dropdown-item {
cursor: pointer;
}
/* For cases where the base Bootstrap 4 equivalent doesn't
work, due to our special uses.*/
.nav-dropdown-menu-right {
@ -241,7 +245,7 @@ input[type="checkbox"] {
/*
* Buttons
*
* Currently used in resultsets only but could be
* Currently used in pushes only but could be
* potentially used elsewhere.
*/

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

@ -32,10 +32,6 @@
background-color: rgba(208, 228, 250, 0.51);
}
.group-btn.btn.job-group-count.selected-count:hover {
background-color: rgb(208, 228, 250);
}
.group-symbol {
background: transparent;
padding: 0 2px 0 2px;
@ -100,10 +96,6 @@
background-color: #fff;
}
.selected-count.btn-lg-xform {
background-color: #fff;
}
.btn-lg-xform {
margin: -2px !important;
border: 2px solid;

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

@ -2,7 +2,7 @@
* Resultset bar
*/
.result-set-bar {
.push-bar {
border-top: 1px solid black;
padding: 2px 0 0 34px;
white-space: nowrap;
@ -12,31 +12,31 @@
justify-content: space-between;
}
.result-set-left {
.push-left {
display: flex;
flex-flow: row wrap;
flex: auto;
align-items: center;
}
.result-set-title-left {
.push-title-left {
flex: 0 0 24.2em;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 10px;
}
.result-set-body-divider {
.push-body-divider {
margin-left: 34px;
border-bottom: 1px solid lightgrey;
}
.result-set-body {
.push-body {
padding-left: 15px;
padding-right: 25px;
}
.result-set {
.push {
padding-bottom: 0;
margin-left: 0;
margin-top: 2px;
@ -44,76 +44,76 @@
width: 100%;
}
.result-counts {
.push-counts {
flex: none;
padding-right: 10px;
}
.result-set-progress {
.push-progress {
color: #6f6d70;
font-style: italic;
margin-left: 10px;
}
.result-set-buttons {
.push-buttons {
margin-right: 25px;
}
.btn-resultset {
.btn-push {
color: #666;
background-color: transparent;
padding-left: 9px;
padding-right: 10px;
}
.btn-resultset.btn:focus {
.btn-push.btn:focus {
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
}
.btn-resultset:hover {
.btn-push:hover {
background-color: #6f6d70;
color: white;
}
.btn-resultset.disabled:hover,
.btn-resultset.disabled:active,
.btn-resultset.disabled.active,
.btn-resultset:disabled:hover,
.btn-resultset:disabled:active,
.btn-resultset:disabled.active,
fieldset[disabled] .btn-resultset:hover {
.btn-push.disabled:hover,
.btn-push.disabled:active,
.btn-push.disabled.active,
.btn-push:disabled:hover,
.btn-push:disabled:active,
.btn-push:disabled.active,
fieldset[disabled] .btn-push:hover {
background-color: #7c7a7d;
border-color: #7c7a7d;
color: white;
}
th-action-button .btn-resultset {
th-action-button .btn-push {
padding-left: 6px;
font-size: 14px;
}
/* Encompasses unknown push,resultset,repo */
/* Encompasses unknown push,repo */
.unknown-message-body {
padding-top: 10px;
}
.result-set .job-list-pad {
.push .job-list-pad {
padding-right: 0;
}
.result-set .job-list-nopad {
.push .job-list-nopad {
padding-left: 20px;
padding-right: 0;
display: block;
}
.result-set .revision-list {
.push .revision-list {
padding-left: 15px;
}
.result-set .cancel-all-jobs-confirm {
padding: 8px 34px 0 34px;
.cancel-all-jobs-confirm-btn {
margin-left: 20px;
}
/*

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

@ -33,8 +33,7 @@ require('jquery.scrollto');
require('./vendor/resizer.js');
// Treeherder React UI
require('./job-view/Repo');
require('./job-view/Push');
require('./job-view/PushList');
// Treeherder JS
require('./js/services/log.js');
@ -42,8 +41,6 @@ require('./js/providers.js');
require('./js/values.js');
require('./js/components/auth.js');
require('./js/directives/treeherder/main.js');
require('./js/directives/treeherder/clonejobs.js');
require('./js/directives/treeherder/resultsets.js');
require('./js/directives/treeherder/top_nav_bar.js');
require('./js/directives/treeherder/bottom_nav_panel.js');
require('./js/services/main.js');
@ -80,7 +77,6 @@ require('./js/controllers/settings.js');
require('./js/controllers/repository.js');
require('./js/controllers/notification.js');
require('./js/controllers/filters.js');
require('./js/controllers/jobs.js');
require('./js/controllers/bugfiler.js');
require('./js/controllers/tcjobactions.js');
require('./plugins/tabs.js');

108
ui/helpers/jobHelper.js Normal file
Просмотреть файл

@ -0,0 +1,108 @@
const btnClasses = {
busted: "btn-red",
exception: "btn-purple",
testfailed: "btn-orange",
usercancel: "btn-pink",
retry: "btn-dkblue",
success: "btn-green",
running: "btn-dkgray",
pending: "btn-ltgray",
superseded: "btn-ltblue",
failures: "btn-red",
'in progress': "btn-dkgray",
};
// Get the CSS class for job buttons as well as jobs that show in the pinboard.
// These also apply to result "groupings" like ``failures`` and ``in progress``
// for the colored filter chicklets on the nav bar.
export const getBtnClass = function getBtnClass(resultState, failureClassificationId) {
let btnClass = btnClasses[resultState] || "btn-default";
// handle if a job is classified
const classificationId = parseInt(failureClassificationId, 10);
if (classificationId > 1) {
btnClass += "-classified";
// autoclassification-only case
if (classificationId === 7) {
btnClass += " autoclassified";
}
}
return btnClass;
};
// The result will be unknown unless the state is complete, so much check both.
// TODO: We should consider storing either pending or running in the result,
// even when the job isn't complete. It would simplify a lot of UI code and
// I can't think of a reason that would hurt anything.
export const getStatus = function getStatus(job) {
return job.state === 'completed' ? job.result : job.state;
};
// Fetch the React instance of an object from a DOM element.
// Credit for this approach goes to SO: https://stackoverflow.com/a/48335220/333614
export const findInstance = function findInstance(el) {
const key = Object.keys(el).find(key => key.startsWith('__reactInternalInstance$'));
if (key) {
const fiberNode = el[key];
return fiberNode && fiberNode.return && fiberNode.return.stateNode;
}
return null;
};
// Fetch the React instance of the currently selected job.
export const findSelectedInstance = function findSelectedInstance() {
const selectedEl = $('.th-view-content').find(".job-btn.selected-job").first();
if (selectedEl.length) {
return findInstance(selectedEl[0]);
}
};
// Fetch the React instance based on the jobId, and if scrollTo is true, then
// scroll it into view.
export const findJobInstance = function findJobInstance(jobId, scrollTo) {
const jobEl = $('.th-view-content')
.find(`button[data-job-id='${jobId}']`)
.first();
if (jobEl.length) {
if (scrollTo) {
scrollToElement(jobEl);
}
return findInstance(jobEl[0]);
}
};
// Scroll the element into view.
// TODO: see if Element.scrollIntoView() can be used here. (bug 1434679)
export const scrollToElement = function scrollToElement(el, duration) {
if (_.isUndefined(duration)) {
duration = 50;
}
if (el.position() !== undefined) {
let scrollOffset = -50;
if (window.innerHeight >= 500 && window.innerHeight < 1000) {
scrollOffset = -100;
} else if (window.innerHeight >= 1000) {
scrollOffset = -200;
}
if (!isOnScreen(el)) {
$('.th-global-content').scrollTo(el, duration, { offset: scrollOffset });
}
}
};
// Check if the element is visible on screen or not.
const isOnScreen = function isOnScreen(el) {
const viewport = {};
viewport.top = $(window).scrollTop() + $('#global-navbar-container').height() + 30;
const filterbarheight = $('.active-filters-bar').height();
viewport.top = filterbarheight > 0 ? viewport.top + filterbarheight : viewport.top;
const updatebarheight = $('.update-alert-panel').height();
viewport.top = updatebarheight > 0 ? viewport.top + updatebarheight : viewport.top;
viewport.bottom = $(window).height() - $('#info-panel').height() - 20;
const bounds = {};
bounds.top = el.offset().top;
bounds.bottom = bounds.top + el.outerHeight();
return ((bounds.top <= viewport.bottom) && (bounds.bottom >= viewport.top));
};

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

@ -0,0 +1,8 @@
export const parseAuthor = function parseAuthor(author) {
const userTokens = author.split(/[<>]+/);
const name = userTokens[0].trim().replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1));
const email = userTokens.length > 1 ? userTokens[1] : '';
return { name, email };
};
export default parseAuthor;

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

@ -25,10 +25,15 @@
</div>
<ng-include src="'partials/main/thTreeherderUpdateBar.html'"></ng-include>
<ng-include src="'partials/main/thActiveFiltersBar.html'"></ng-include>
<div id="th-global-content" class="th-global-content"
ng-click="clearJobOnClick($event)">
<div id="th-global-content" class="th-global-content">
<span class="th-view-content" ng-cloak>
<ng-view></ng-view>
<push-list
user="user"
repoName="repoName"
revision="revision"
currentRepo="currentRepo"
watchDepth="reference"
/>
</span>
</div>
<div ng-controller="PluginCtrl"

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

@ -1,31 +1,38 @@
import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
import { getBtnClass, findJobInstance } from "../helpers/jobHelper";
const mapStateToProps = ({ pushes }) => pushes;
class JobButtonComponent extends React.Component {
export default class JobButtonComponent extends React.Component {
constructor(props) {
super(props);
this.$rootScope = this.props.$injector.get('$rootScope');
this.thEvents = this.props.$injector.get('thEvents');
this.thResultStatus = this.props.$injector.get('thResultStatus');
this.thResultStatusInfo = this.props.$injector.get('thResultStatusInfo');
this.ThJobModel = this.props.$injector.get('ThJobModel');
this.ThResultSetStore = this.props.$injector.get('ThResultSetStore');
const { $injector } = this.props;
this.$rootScope = $injector.get('$rootScope');
this.thEvents = $injector.get('thEvents');
this.ThResultSetStore = $injector.get('ThResultSetStore');
this.state = {
failureClassificationId: null
isSelected: false,
isRunnableSelected: false,
};
}
componentWillMount() {
this.$rootScope.$on(this.thEvents.jobsClassified, (ev, { jobs }) => {
const ids = Object.keys(jobs).map(job => parseInt(job));
if (ids.includes(this.props.job.id)) {
const job = jobs[this.props.job.id];
this.setState({ failureClassificationId: job.failure_classification_id });
}
});
const { job } = this.props;
const { id } = job;
const urlSelectedJob = new URLSearchParams(
location.hash.split('?')[1]).get('selectedJob');
if (parseInt(urlSelectedJob) === id) {
this.setState({ isSelected: true });
}
}
componentDidMount() {
if (this.state.isSelected) {
// scroll to make this job if it's selected
findJobInstance(this.props.job.id, true);
}
}
/**
@ -36,66 +43,85 @@ class JobButtonComponent extends React.Component {
* logic on shouldComponentUpdate is a little more complex than a simple
* shallow compare would allow.
*/
shouldComponentUpdate(nextProps) {
return (this.props.job.id === nextProps.selectedJobId ||
this.props.job.id === this.props.selectedJobId ||
this.props.visible !== nextProps.visible ||
this.props.job.selected !== nextProps.selected);
shouldComponentUpdate(nextProps, nextState) {
const { visible, status, failureClassificationId } = this.props;
const { isSelected, isRunnableSelected } = this.state;
return (
visible !== nextProps.visible ||
status !== nextProps.status ||
failureClassificationId !== nextProps.failureClassificationId ||
isSelected !== nextState.isSelected ||
isRunnableSelected !== nextState.isRunnableSelected
);
}
componentWillUnmount() {
this.setState({ isRunnableSelected: false, isSelected: false });
}
setSelected(isSelected) {
this.setState({ isSelected });
}
toggleRunnableSelected() {
this.setState({ isRunnableSelected: !this.state.isRunnableSelected });
}
render() {
if (!this.props.job.visible) return null;
const status = this.thResultStatus(this.props.job);
const runnable = this.props.job.state === 'runnable';
const statusInfo = this.thResultStatusInfo(status, this.props.job.failure_classification_id);
let title = `${this.props.job.job_type_name} - ${status}`;
const { job } = this.props;
const { isSelected, isRunnableSelected } = this.state;
const { state, job_type_name, failure_classification_id, end_timestamp,
start_timestamp, ref_data_name, visible, id,
job_type_symbol, result } = job;
if (this.props.job.state === 'completed') {
const duration = Math.round((this.props.job.end_timestamp - this.props.job.start_timestamp) / 60);
if (!visible) return null;
const resultState = state === "completed" ? result : state;
const runnable = state === 'runnable';
const btnClass = getBtnClass(resultState, failure_classification_id);
let title = `${job_type_name} - ${status}`;
if (state === 'completed') {
const duration = Math.round((end_timestamp - start_timestamp) / 60);
title += ` (${duration} mins)`;
}
const classes = ['btn', statusInfo.btnClass];
const classes = ['btn', btnClass, 'filter-shown'];
const attributes = {
'data-job-id': id,
'data-ignore-job-clear-on-click': true,
title
};
if (runnable) {
classes.push('runnable-job-btn', 'runnable');
attributes['data-buildername'] = ref_data_name;
if (isRunnableSelected) {
classes.push('runnable-job-btn-selected');
}
} else {
classes.push('job-btn');
}
if (runnable) {
if (runnable && this.ThResultSetStore.isRunnableJobSelected(this.$rootScope.repoName,
this.props.job.result_set_id,
this.props.job.ref_data_name)) {
classes.push('runnable-job-btn-selected');
}
}
if (this.state.selected || this.props.job.id === this.props.selectedJobId) {
if (isSelected) {
classes.push('selected-job btn-lg-xform');
} else {
classes.push('btn-xs');
}
if (this.props.job.visible) classes.push('filter-shown');
const attributes = {
className: classes.join(' '),
'data-job-id': this.props.job.id,
'data-ignore-job-clear-on-click': true,
title
};
if (runnable) {
attributes['data-buildername'] = this.props.job.ref_data_name;
}
return <button {...attributes}>{this.props.job.job_type_symbol}</button>;
attributes.className = classes.join(' ');
return (
<button {...attributes}>{job_type_symbol}</button>
);
}
}
JobButtonComponent.propTypes = {
$injector: PropTypes.object.isRequired,
job: PropTypes.object.isRequired,
$injector: PropTypes.object.isRequired,
repoName: PropTypes.string.isRequired,
visible: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
failureClassificationId: PropTypes.number, // runnable jobs won't have this
hasGroup: PropTypes.bool.isRequired,
};
export default connect(mapStateToProps)(JobButtonComponent);

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

@ -1,5 +1,8 @@
import React from 'react';
export default (props) => {
const classes = [props.className, 'btn group-btn btn-xs job-group-count filter-shown'];
return (
<button
className={classes.join(' ')}

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

@ -2,33 +2,33 @@ import PropTypes from 'prop-types';
import * as _ from 'lodash';
import JobButton from './JobButton';
import JobCountComponent from './JobCount';
import { getBtnClass, getStatus } from "../helpers/jobHelper";
export default class JobGroup extends React.Component {
constructor(props) {
super(props);
this.$rootScope = this.props.$injector.get('$rootScope');
this.thEvents = this.props.$injector.get('thEvents');
this.thResultStatus = this.props.$injector.get('thResultStatus');
this.thResultStatusInfo = this.props.$injector.get('thResultStatusInfo');
const { $injector } = this.props;
this.$rootScope = $injector.get('$rootScope');
this.thEvents = $injector.get('thEvents');
// The group should be expanded initially if the global group state is expanded
const groupState = new URLSearchParams(location.hash.split('?')[1]).get('group_state');
const duplicateJobs = new URLSearchParams(location.hash.split('?')[1]).get('duplicate_jobs');
this.state = {
expanded: this.props.expanded || groupState === 'expanded',
showDuplicateJobs: false
expanded: groupState === 'expanded',
showDuplicateJobs: duplicateJobs === 'visible',
};
}
componentWillMount() {
this.$rootScope.$on(
this.duplicateJobsVisibilityChangedUnlisten = this.$rootScope.$on(
this.thEvents.duplicateJobsVisibilityChanged,
() => {
this.setState({ showDuplicateJobs: !this.state.showDuplicateJobs });
}
);
this.$rootScope.$on(
this.groupStateChangedUnlisten = this.$rootScope.$on(
this.thEvents.groupStateChanged,
(e, newState) => {
this.setState({ expanded: newState === 'expanded' });
@ -37,23 +37,31 @@ export default class JobGroup extends React.Component {
this.toggleExpanded = this.toggleExpanded.bind(this);
}
componentWillUnmount() {
this.duplicateJobsVisibilityChangedUnlisten();
this.groupStateChangedUnlisten();
}
toggleExpanded() {
this.setState({ expanded: !this.state.expanded });
}
groupButtonsAndCounts() {
groupButtonsAndCounts(jobs) {
let buttons = [];
const counts = [];
const stateCounts = {};
if (this.state.expanded) {
// All buttons should be shown when the group is expanded
buttons = this.props.group.jobs;
buttons = jobs;
} else {
const typeSymbolCounts = _.countBy(this.props.group.jobs, 'job_type_symbol');
this.props.group.jobs.forEach((job) => {
const typeSymbolCounts = _.countBy(jobs, 'job_type_symbol');
jobs.forEach((job) => {
if (!job.visible) return;
const status = this.thResultStatus(job);
let countInfo = this.thResultStatusInfo(status, job.failure_classification_id);
const status = getStatus(job);
let countInfo = {
btnClass: getBtnClass(status, job.failure_classification_id),
countText: status
};
if (['testfailed', 'busted', 'exception'].includes(status) ||
(typeSymbolCounts[job.job_type_symbol] > 1 && this.state.showDuplicateJobs)) {
// render the job itself, not a count
@ -87,21 +95,22 @@ export default class JobGroup extends React.Component {
}
render() {
this.items = this.groupButtonsAndCounts();
const { group, $injector, repoName } = this.props;
this.items = this.groupButtonsAndCounts(group.jobs);
return (
<span className="platform-group">
<span
className="disabled job-group"
title={this.props.group.name}
data-grkey={this.props.group.grkey}
title={group.name}
data-grkey={group.grkey}
>
<button
className="btn group-symbol"
data-ignore-job-clear-on-click
onClick={this.toggleExpanded}
>{this.props.group.symbol}{this.props.group.tier &&
<span className="small text-muted">[tier {this.props.group.tier}]</span>
>{group.symbol}{group.tier &&
<span className="small text-muted">[tier {group.tier}]</span>
}</button>
<span className="group-content">
@ -109,8 +118,11 @@ export default class JobGroup extends React.Component {
{this.items.buttons.map((job, i) => (
<JobButton
job={job}
$injector={this.props.$injector}
$injector={$injector}
visible={job.visible}
status={getStatus(job)}
failureClassificationId={job.failure_classification_id}
repoName={repoName}
hasGroup
key={job.id}
ref={i}
@ -139,6 +151,6 @@ export default class JobGroup extends React.Component {
JobGroup.propTypes = {
group: PropTypes.object.isRequired,
repoName: PropTypes.string.isRequired,
$injector: PropTypes.object.isRequired,
expanded: PropTypes.bool,
};

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

@ -2,21 +2,24 @@ import PropTypes from 'prop-types';
import React from 'react';
import JobButton from './JobButton';
import JobGroup from './JobGroup';
import { getStatus } from "../helpers/jobHelper";
export default class JobsAndGroups extends React.Component {
render() {
const { $injector, groups, repoName } = this.props;
return (
<td className="job-row">
{this.props.groups.map((group, i) => {
{groups.map((group, i) => {
if (group.symbol !== '?') {
return (
group.visible && <JobGroup
group={group}
$injector={this.props.$injector}
repoName={repoName}
$injector={$injector}
refOrder={i}
key={group.mapKey}
ref={i}
expanded={this.props.expanded}
/>
);
}
@ -24,10 +27,13 @@ export default class JobsAndGroups extends React.Component {
group.jobs.map(job => (
<JobButton
job={job}
$injector={this.props.$injector}
$injector={$injector}
repoName={repoName}
visible={job.visible}
key={job.id}
status={getStatus(job)}
failureClassificationId={job.failure_classification_id}
hasGroup={false}
key={job.id}
ref={i}
refOrder={i}
/>
@ -41,5 +47,6 @@ export default class JobsAndGroups extends React.Component {
JobsAndGroups.propTypes = {
groups: PropTypes.array.isRequired,
repoName: PropTypes.string.isRequired,
$injector: PropTypes.object.isRequired,
};

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

@ -13,12 +13,15 @@ const PlatformName = (props) => {
export default class Platform extends React.Component {
render() {
const { platform, $injector, repoName } = this.props;
return (
<tr id={this.props.platform.id} key={this.props.platform.id}>
<PlatformName platform={this.props.platform} />
<tr id={platform.id} key={platform.id}>
<PlatformName platform={platform} />
<JobsAndGroups
groups={this.props.platform.groups}
$injector={this.props.$injector}
groups={platform.groups}
repoName={repoName}
$injector={$injector}
/>
</tr>
);
@ -27,6 +30,7 @@ export default class Platform extends React.Component {
Platform.propTypes = {
platform: PropTypes.object.isRequired,
repoName: PropTypes.string.isRequired,
$injector: PropTypes.object.isRequired,
};

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

@ -1,47 +1,91 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import PushJobs from './PushJobs';
import PushHeader from './PushHeader';
import { RevisionList } from './RevisionList';
import { store } from './redux/store';
import * as aggregateIds from './aggregateIds';
class Push extends React.Component {
export default class Push extends React.Component {
constructor(props) {
super(props);
this.$rootScope = this.props.$injector.get('$rootScope');
this.aggregateId = aggregateIds.getResultsetTableId(
this.$rootScope.repoName, this.props.push.id, this.props.push.revision
const { $injector, repoName, push } = props;
this.$rootScope = $injector.get('$rootScope');
this.thEvents = $injector.get('thEvents');
this.ThResultSetStore = $injector.get('ThResultSetStore');
this.aggregateId = aggregateIds.getPushTableId(
repoName, push.id, push.revision
);
this.showRunnableJobs = this.showRunnableJobs.bind(this);
this.hideRunnableJobs = this.hideRunnableJobs.bind(this);
this.state = {
runnableVisible: false
};
}
showRunnableJobs() {
this.$rootScope.$emit(this.thEvents.showRunnableJobs, this.props.push.id);
this.setState({ runnableVisible: true });
}
hideRunnableJobs() {
this.ThResultSetStore.deleteRunnableJobs(this.props.repoName, this.props.push.id);
this.$rootScope.$emit(this.thEvents.deleteRunnableJobs, this.props.push.id);
this.setState({ runnableVisible: false });
}
render() {
const { push, loggedIn, isStaff, isTryRepo, $injector, repoName } = this.props;
const { currentRepo, urlBasePath } = this.$rootScope;
const { id, push_timestamp, revision, job_counts, author } = push;
return (
<Provider store={store}>
<div className="row result-set clearfix">
{this.$rootScope.currentRepo &&
<RevisionList
push={this.props.push}
$injector={this.props.$injector}
repo={this.$rootScope.currentRepo}
/>}
<div className="push">
<PushHeader
pushId={id}
pushTimestamp={push_timestamp}
author={author}
revision={revision}
jobCounts={job_counts}
loggedIn={loggedIn}
isStaff={isStaff}
repoName={repoName}
isTryRepo={isTryRepo}
urlBasePath={urlBasePath}
$injector={$injector}
runnableVisible={this.state.runnableVisible}
showRunnableJobsCb={this.showRunnableJobs}
hideRunnableJobsCb={this.hideRunnableJobs}
/>
<div className="push-body-divider" />
<div className="row push clearfix">
{currentRepo &&
<RevisionList
push={push}
$injector={$injector}
repo={currentRepo}
/>
}
<span className="job-list job-list-pad col-7">
<PushJobs
push={this.props.push}
$injector={this.props.$injector}
push={push}
repoName={repoName}
$injector={$injector}
/>
</span>
</div>
</Provider>
</div>
);
}
}
Push.propTypes = {
push: PropTypes.object.isRequired,
isTryRepo: PropTypes.bool,
loggedIn: PropTypes.bool,
isStaff: PropTypes.bool,
repoName: PropTypes.string,
$injector: PropTypes.object.isRequired,
};
treeherder.directive('push', ['reactDirective', '$injector', (reactDirective, $injector) =>
reactDirective(Push, ['push'], {}, { $injector, store })]);

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

@ -0,0 +1,171 @@
import React from "react";
import PropTypes from 'prop-types';
export default class PushActionMenu extends React.PureComponent {
constructor(props) {
super(props);
const { $injector } = this.props;
this.$rootScope = $injector.get('$rootScope');
this.thEvents = $injector.get('thEvents');
this.thNotify = $injector.get('thNotify');
this.thJobFilters = $injector.get('thJobFilters');
this.ThResultSetStore = $injector.get('ThResultSetStore');
this.ThModelErrors = $injector.get('ThModelErrors');
this.ThTaskclusterErrors = $injector.get('ThTaskclusterErrors');
// customPushActions uses $uibModal which doesn't work well in the
// unit tests. So if we fail to inject here, that's OK.
// Bug 1437736
try {
this.customPushActions = $injector.get('customPushActions');
} catch (ex) {
this.customPushActions = {};
}
this.revision = this.props.revision;
this.pushId = this.props.pushId;
this.repoName = this.props.repoName;
this.triggerMissingJobs = this.triggerMissingJobs.bind(this);
this.triggerAllTalosJobs = this.triggerAllTalosJobs.bind(this);
// Trigger missing jobs is dangerous on repos other than these (see bug 1335506)
this.triggerMissingRepos = ['mozilla-inbound', 'autoland'];
}
triggerMissingJobs() {
if (!window.confirm(`This will trigger all missing jobs for revision ${this.revision}!\n\nClick "OK" if you want to proceed.`)) {
return;
}
this.ThResultSetStore.getGeckoDecisionTaskId(this.repoName, this.pushId)
.then((decisionTaskID) => {
this.ThResultSetModel.triggerMissingJobs(decisionTaskID)
.then((msg) => {
this.thNotify.send(msg, "success");
}, (e) => {
this.thNotify.send(
this.ThModelErrors.format(e, "The action 'trigger missing jobs' failed"),
'danger', true
);
});
});
}
triggerAllTalosJobs() {
if (!window.confirm(`This will trigger all Talos jobs for revision ${this.revision}!\n\nClick "OK" if you want to proceed.`)) {
return;
}
let times = parseInt(window.prompt("Enter number of instances to have for each talos job", 6));
while (times < 1 || times > 6 || isNaN(times)) {
times = window.prompt("We only allow instances of each talos job to be between 1 to 6 times. Enter again", 6);
}
this.ThResultSetStore.getGeckoDecisionTaskId(this.repoName, this.pushId)
.then((decisionTaskID) => {
this.ThResultSetModel.triggerAllTalosJobs(times, decisionTaskID)
.then((msg) => {
this.thNotify.send(msg, "success");
}, (e) => {
this.thNotify.send(
this.ThTaskclusterErrors.format(e),
'danger',
{ sticky: true }
);
});
});
}
render() {
const { loggedIn, isStaff, repoName, revision, pushId, runnableVisible,
hideRunnableJobsCb, showRunnableJobsCb } = this.props;
const { addFilter } = this.thJobFilters;
return (
<span className="btn-group dropdown" dropdown="true">
<button
dropdown-toggle="true"
className="btn btn-sm btn-push dropdown-toggle"
type="button"
title="Action menu"
data-hover="dropdown"
data-toggle="dropdown"
data-delay="1000"
data-ignore-job-clear-on-click="true"
>
<span className="caret" data-ignore-job-clear-on-click="true" />
</button>
<ul className="dropdown-menu pull-right">
{runnableVisible ?
<li data-ignore-job-clear-on-click="true"
title="Hide Runnable Jobs"
className="dropdown-item"
onClick={() => hideRunnableJobsCb()}
>Hide Runnable Jobs</li> :
<li
title={loggedIn ? 'Add new jobs to this push' : 'Must be logged in'}
className={loggedIn ? 'dropdown-item' : 'dropdown-item disabled'}
data-ignore-job-clear-on-click
onClick={() => showRunnableJobsCb()}
>Add new jobs</li>
}
<li
data-ignore-job-clear-on-click="true"
className="dropdown-item"
href={`https://secure.pub.build.mozilla.org/buildapi/self-serve/${repoName}/rev/${revision}`}
>BuildAPI</li>
{isStaff && this.triggerMissingRepos.includes(repoName) &&
<li
data-ignore-job-clear-on-click="true"
className="dropdown-item"
onClick={() => this.triggerMissingJobs(revision)}
>Trigger missing jobs</li>
}
{isStaff &&
<li
data-ignore-job-clear-on-click="true"
className="dropdown-item"
onClick={() => this.triggerAllTalosJobs(revision)}
>Trigger all Talos jobs</li>
}
<li>
<a
target="_blank" data-ignore-job-clear-on-click="true"
rel="noopener noreferrer"
className="dropdown-item"
href={`https://bugherder.mozilla.org/?cset=${revision}&amp;tree=${repoName}`}
title="Use Bugherder to mark the bugs in this push"
>Mark with Bugherder</a></li>
<li
data-ignore-job-clear-on-click="true"
className="dropdown-item"
onClick={() => this.customPushActions.open(repoName, pushId)}
title="View/Edit/Submit Action tasks for this push"
>Custom Push Action...</li>
<li
className="dropdown-item"
prevent-default-on-left-click="true"
onClick={() => addFilter('tochange', revision)}
>Set as top of range</li>
<li
className="dropdown-item"
prevent-default-on-left-click="true"
onClick={() => addFilter('fromchange', revision)}
>Set as bottom of range</li>
</ul>
</span>
);
}
}
PushActionMenu.propTypes = {
runnableVisible: PropTypes.bool.isRequired,
isStaff: PropTypes.bool.isRequired,
loggedIn: PropTypes.bool.isRequired,
revision: PropTypes.string.isRequired,
};

268
ui/job-view/PushHeader.jsx Normal file
Просмотреть файл

@ -0,0 +1,268 @@
import React from "react";
import PropTypes from 'prop-types';
import { Alert } from 'reactstrap';
import PushActionMenu from './PushActionMenu';
const Author = (props) => {
const authorMatch = props.author.match(/\<(.*?)\>+/);
const authorEmail = authorMatch ? authorMatch[1] : props.author;
return (
<span title="View pushes by this user" className="push-author">
<a href={props.url} data-ignore-job-clear-on-click>{authorEmail}</a>
</span>
);
};
const PushCounts = (props) => {
const { pending, running, completed } = props;
const inProgress = pending + running;
const total = completed + inProgress;
const percentComplete = total > 0 ?
Math.floor(((completed / total) * 100)) : undefined;
return (
<span className="push-progress">
{percentComplete === 100 ?
<span>- Complete -</span> :
<span
title="Proportion of jobs that are complete"
>{percentComplete}% - {inProgress} in progress</span>
}
</span>
);
};
export default class PushHeader extends React.PureComponent {
constructor(props) {
super(props);
const { $injector, pushTimestamp, urlBasePath, repoName, revision, author } = this.props;
this.$rootScope = $injector.get('$rootScope');
this.thEvents = $injector.get('thEvents');
this.thJobFilters = $injector.get('thJobFilters');
this.thNotify = $injector.get('thNotify');
this.thPinboard = $injector.get('thPinboard');
this.thPinboardCountError = $injector.get('thPinboardCountError');
this.thBuildApi = $injector.get('thBuildApi');
this.ThResultSetStore = $injector.get('ThResultSetStore');
this.ThResultSetModel = $injector.get('ThResultSetModel');
this.ThModelErrors = $injector.get('ThModelErrors');
this.ThTaskclusterErrors = $injector.get('ThTaskclusterErrors');
const dateFormat = { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false };
this.pushDateStr = new Date(pushTimestamp*1000).toLocaleString("en-US", dateFormat);
this.revisionPushFilterUrl = `${urlBasePath}?repo=${repoName}&revision=${revision}`;
this.authorPushFilterUrl = `${urlBasePath}?repo=${repoName}&author=${encodeURIComponent(author)}`;
this.pinAllShownJobs = this.pinAllShownJobs.bind(this);
this.triggerNewJobs = this.triggerNewJobs.bind(this);
this.cancelAllJobs = this.cancelAllJobs.bind(this);
this.state = {
showConfirmCancelAll: false,
runnableJobsSelected: false,
};
}
componentWillMount() {
this.toggleRunnableJobUnlisten = this.$rootScope.$on(
this.thEvents.selectRunnableJob, (ev, runnableJobs, pushId) => {
if (this.props.pushId === pushId) {
this.setState({ runnableJobsSelected: runnableJobs.length > 0 });
}
}
);
}
componentWillUnmount() {
this.toggleRunnableJobUnlisten();
}
filterParams() {
return Object.entries(this.thJobFilters.getActiveFilters())
.reduce((acc, [key, value]) => `&${key}=${value}`, "");
}
triggerNewJobs() {
const { repoName, loggedIn, pushId } = this.props;
if (!window.confirm(
'This will trigger all selected jobs. Click "OK" if you want to proceed.')) {
return;
}
if (loggedIn) {
const builderNames = this.ThResultSetStore.getSelectedRunnableJobs(repoName, pushId);
this.ThResultSetStore.getGeckoDecisionTaskId(repoName, pushId).then((decisionTaskID) => {
this.ThResultSetModel.triggerNewJobs(builderNames, decisionTaskID).then((result) => {
this.thNotify.send(result, "success");
this.ThResultSetStore.deleteRunnableJobs(repoName, pushId);
this.props.hideRunnableJobsCb();
this.setState({ runnableJobsSelected: false });
}, (e) => {
this.thNotify.send(this.ThTaskclusterErrors.format(e), 'danger', { sticky: true });
});
});
} else {
this.thNotify.send("Must be logged in to trigger a job", 'danger');
}
}
cancelAllJobs() {
const { repoName, revision, isTryRepo, isStaff, pushId } = this.props;
this.setState({ showConfirmCancelAll: false });
if (!(isTryRepo || isStaff)) return;
this.ThResultSetModel.cancelAll(pushId, repoName).then(() => (
this.thBuildApi.cancelAll(repoName, revision)
)).catch((e) => {
this.thNotify.send(
this.ThModelErrors.format(e, "Failed to cancel all jobs"),
'danger', true
);
});
}
pinAllShownJobs() {
if (!this.thPinboard.spaceRemaining()) {
this.thNotify.send(this.thPinboardCountError, 'danger');
return;
}
const shownJobs = this.ThResultSetStore.getAllShownJobs(
this.props.repoName,
this.thPinboard.spaceRemaining(),
this.thPinboardCountError,
this.props.pushId
);
this.thPinboard.pinJobs(shownJobs);
if (!this.$rootScope.selectedJob) {
this.$rootScope.$emit(this.thEvents.jobClick, shownJobs[0]);
}
}
render() {
const { repoName, loggedIn, pushId, isTryRepo, isStaff, jobCounts, author,
revision, runnableVisible, $injector,
showRunnableJobsCb, hideRunnableJobsCb } = this.props;
const cancelJobsTitle = loggedIn ?
"Cancel all jobs" :
"Must be logged in to cancel jobs";
const canCancelJobs = isTryRepo || isStaff;
const counts = jobCounts || { pending: 0, running: 0, completed: 0 };
return (
<div className="push-header">
<div className="push-bar" key="push-header">
<span className="push-left">
<span className="push-title-left">
<span>
<a
href={`${this.revisionPushFilterUrl}${this.filterParams()}`}
title="View only this push"
data-ignore-job-clear-on-click
>{this.pushDateStr} <span className="fa fa-external-link icon-superscript" />
</a> - </span>
<Author author={author} url={this.authorPushFilterUrl} />
</span>
</span>
<PushCounts
className="push-counts"
pending={counts.pending}
running={counts.running}
completed={counts.completed}
/>
<span className="push-buttons">
<a
className="btn btn-sm btn-push test-view-btn"
href={`/testview.html?repo=${repoName}&revision=${revision}`}
target="_blank"
title="View details on failed test results for this push"
>View Tests</a>
{canCancelJobs &&
<button
className="btn btn-sm btn-push cancel-all-jobs-btn"
title={cancelJobsTitle}
data-ignore-job-clear-on-click
onClick={() => this.setState({ showConfirmCancelAll: true })}
>
<span
className="fa fa-times-circle cancel-job-icon dim-quarter"
data-ignore-job-clear-on-click
/>
</button>
}
<button
className="btn btn-sm btn-push pin-all-jobs-btn"
title="Pin all available jobs in this push"
data-ignore-job-clear-on-click
onClick={this.pinAllShownJobs}
>
<span
className="fa fa-thumb-tack"
data-ignore-job-clear-on-click
/>
</button>
{this.state.runnableJobsSelected && runnableVisible &&
<button
className="btn btn-sm btn-push trigger-new-jobs-btn"
title="Trigger new jobs"
data-ignore-job-clear-on-click
onClick={this.triggerNewJobs}
>Trigger New Jobs</button>
}
<PushActionMenu
loggedIn={loggedIn}
isStaff={isStaff || false}
runnableVisible={runnableVisible}
revision={revision}
repoName={repoName}
pushId={pushId}
$injector={$injector}
showRunnableJobsCb={showRunnableJobsCb}
hideRunnableJobsCb={hideRunnableJobsCb}
/>
</span>
</div>
{this.state.showConfirmCancelAll &&
<div
className="cancel-all-jobs-confirm animate-show"
key="cancelConfirm"
>
<Alert color="danger" toggle={() => this.setState({ showConfirmCancelAll: false })}>
<span className="fa fa-exclamation-triangle" />
<span> This action will cancel all pending and running jobs for this push. <i>It cannot be undone!</i>
</span>
<button
onClick={this.cancelAllJobs}
className="btn btn-xs btn-danger cancel-all-jobs-confirm-btn"
>Confirm</button>
</Alert>
</div>
}
</div>
);
}
}
PushHeader.propTypes = {
pushId: PropTypes.number.isRequired,
pushTimestamp: PropTypes.number.isRequired,
author: PropTypes.string.isRequired,
revision: PropTypes.string.isRequired,
jobCounts: PropTypes.object,
loggedIn: PropTypes.bool,
isStaff: PropTypes.bool,
repoName: PropTypes.string.isRequired,
isTryRepo: PropTypes.bool,
urlBasePath: PropTypes.string,
$injector: PropTypes.object.isRequired,
runnableVisible: PropTypes.bool.isRequired,
showRunnableJobsCb: PropTypes.func.isRequired,
hideRunnableJobsCb: PropTypes.func.isRequired,
};

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

@ -1,43 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import * as _ from 'lodash';
import { actions, store } from './redux/store';
import { platformMap } from '../js/constants';
import * as aggregateIds from './aggregateIds';
import Platform from './Platform';
import { findInstance, findSelectedInstance, findJobInstance } from '../helpers/jobHelper';
export default class PushJobs extends React.Component {
constructor(props) {
super(props);
this.$rootScope = this.props.$injector.get('$rootScope');
this.$location = this.props.$injector.get('$location');
this.thEvents = this.props.$injector.get('thEvents');
this.ThResultSetStore = this.props.$injector.get('ThResultSetStore');
this.ThJobModel = this.props.$injector.get('ThJobModel');
this.thUrl = this.props.$injector.get('thUrl');
this.thJobFilters = this.props.$injector.get('thJobFilters');
this.thResultStatus = this.props.$injector.get('thResultStatus');
this.thResultStatusInfo = this.props.$injector.get('thResultStatusInfo');
const { $injector, push, repoName } = this.props;
this.rsMap = null;
this.pushId = this.props.push.id;
this.aggregateId = aggregateIds.getResultsetTableId(
this.$rootScope.repoName,
this.$rootScope = $injector.get('$rootScope');
this.$location = $injector.get('$location');
this.thEvents = $injector.get('thEvents');
this.ThResultSetStore = $injector.get('ThResultSetStore');
this.ThJobModel = $injector.get('ThJobModel');
this.thUrl = $injector.get('thUrl');
this.thJobFilters = $injector.get('thJobFilters');
this.pushId = push.id;
this.aggregateId = aggregateIds.getPushTableId(
repoName,
this.pushId,
this.props.push.revision
push.revision
);
this.state = { platforms: null, isRunnableVisible: false };
this.onMouseDown = this.onMouseDown.bind(this);
this.selectJob = this.selectJob.bind(this);
const showDuplicateJobs = this.$location.search().duplicate_jobs === 'visible';
const expanded = this.$location.search().group_state === 'expanded';
store.dispatch(actions.pushes.setCountExpanded(expanded));
store.dispatch(actions.pushes.setShowDuplicates(showDuplicateJobs));
this.state = {
platforms: null,
isRunnableVisible: false,
};
}
componentWillMount() {
this.applyNewJobs();
}
componentDidMount() {
this.$rootScope.$on(
this.applyNewJobsUnlisten = this.$rootScope.$on(
this.thEvents.applyNewJobs, (ev, appliedpushId) => {
if (appliedpushId === this.pushId) {
this.applyNewJobs();
@ -45,54 +48,61 @@ export default class PushJobs extends React.Component {
}
);
this.$rootScope.$on(
this.thEvents.clearSelectedJob, () => {
store.dispatch(actions.pushes.setSelectedJobId(null));
}
);
this.$rootScope.$on(
this.globalFilterChangedUnlisten = this.$rootScope.$on(
this.thEvents.globalFilterChanged, () => {
this.filterJobs();
}
);
this.$rootScope.$on(
this.groupStateChangedUnlisten = this.$rootScope.$on(
this.thEvents.groupStateChanged, () => {
this.filterJobs();
}
);
this.$rootScope.$on(
this.jobsClassifiedUnlisten = this.$rootScope.$on(
this.thEvents.jobsClassified, () => {
this.filterJobs();
}
);
this.$rootScope.$on(
this.searchPageUnlisten = this.$rootScope.$on(
this.thEvents.searchPage, () => {
this.filterJobs();
}
);
this.$rootScope.$on(this.thEvents.showRunnableJobs, (ev, push) => {
if (this.props.push.id === push.id) {
this.showRunnableJobsUnlisten = this.$rootScope.$on(this.thEvents.showRunnableJobs, (ev, pushId) => {
const { push, repoName } = this.props;
if (push.id === pushId) {
push.isRunnableVisible = true;
this.setState({ isRunnableVisible: true });
this.ThResultSetStore.addRunnableJobs(this.$rootScope.repoName, push);
this.ThResultSetStore.addRunnableJobs(repoName, push);
}
});
this.$rootScope.$on(this.thEvents.deleteRunnableJobs, (ev, push) => {
if (this.props.push.id === push.id) {
this.deleteRunnableJobsUnlisten = this.$rootScope.$on(this.thEvents.deleteRunnableJobs, (ev, pushId) => {
const { push } = this.props;
if (push.id === pushId) {
push.isRunnableVisible = false;
this.setState({ isRunnableVisible: false });
store.dispatch(actions.pushes.setSelectedRunnableJobs(null));
this.applyNewJobs();
}
});
}
componentWillUnmount() {
this.applyNewJobsUnlisten();
this.globalFilterChangedUnlisten();
this.groupStateChangedUnlisten();
this.jobsClassifiedUnlisten();
this.searchPageUnlisten();
this.showRunnableJobsUnlisten();
this.deleteRunnableJobsUnlisten();
}
onMouseDown(ev) {
const jobElem = ev.target.attributes.getNamedItem('data-job-id');
if (jobElem) {
@ -105,14 +115,14 @@ export default class PushJobs extends React.Component {
} else if (job.state === 'runnable') { // Toggle runnable
this.handleRunnableClick(job);
} else {
this.selectJob(job); // Left click
this.selectJob(job, ev.target); // Left click
}
}
}
getIdForPlatform(platform) {
return aggregateIds.getPlatformRowId(
this.$rootScope.repoName,
this.props.repoName,
this.props.push.id,
platform.name,
platform.option
@ -120,7 +130,7 @@ export default class PushJobs extends React.Component {
}
getJobFromId(jobId) {
const jobMap = this.ThResultSetStore.getJobMap(this.$rootScope.repoName);
const jobMap = this.ThResultSetStore.getJobMap(this.props.repoName);
return jobMap[`${jobId}`].job_obj;
}
@ -132,18 +142,22 @@ export default class PushJobs extends React.Component {
this.setState({ platforms });
}
selectJob(job) {
store.dispatch(actions.pushes.setSelectedJobId(job.id));
selectJob(job, el) {
const selected = findSelectedInstance();
if (selected) selected.setSelected(false);
const jobInstance = findInstance(el);
jobInstance.setSelected(true);
this.$rootScope.$emit(this.thEvents.jobClick, job);
}
applyNewJobs() {
this.rsMap = this.ThResultSetStore.getResultSetsMap(this.$rootScope.repoName);
if (!this.rsMap[this.pushId] || !this.rsMap[this.pushId].rs_obj.platforms) {
const { push } = this.props;
if (!push.platforms) {
return;
}
const rsPlatforms = this.rsMap[this.pushId].rs_obj.platforms;
const rsPlatforms = push.platforms;
const platforms = rsPlatforms.reduce((acc, platform) => {
const thisPlatform = { ...platform };
thisPlatform.id = this.getIdForPlatform(platform);
@ -162,7 +176,7 @@ export default class PushJobs extends React.Component {
handleLogViewerClick(jobId) {
// Open logviewer in a new window
this.ThJobModel.get(
this.$rootScope.repoName,
this.props.repoName,
jobId
).then((data) => {
if (data.logs.length > 0) {
@ -173,12 +187,12 @@ export default class PushJobs extends React.Component {
}
handleRunnableClick(job) {
const selected = this.ThResultSetStore.toggleSelectedRunnableJob(
this.$rootScope.repoName,
this.ThResultSetStore.toggleSelectedRunnableJob(
this.props.repoName,
this.pushId,
job.ref_data_name
);
store.dispatch(actions.pushes.setSelectedRunnableJobs({ selectedRunnableJobs: selected }));
findJobInstance(job.id, false).toggleRunnableSelected();
}
filterPlatform(platform) {
@ -187,9 +201,8 @@ export default class PushJobs extends React.Component {
group.visible = false;
group.jobs.forEach((job) => {
job.visible = this.thJobFilters.showJob(job);
if (this.rsMap && job.state === 'runnable') {
job.visible = job.visible &&
this.rsMap[job.result_set_id].rs_obj.isRunnableVisible;
if (job.state === 'runnable') {
job.visible = job.visible && this.props.push.isRunnableVisible;
}
job.selected = this.$rootScope.selectedJob ? job.id === this.$rootScope.selectedJob.id : false;
if (job.visible) {
@ -203,6 +216,8 @@ export default class PushJobs extends React.Component {
render() {
const platforms = this.state.platforms || {};
const { $injector, repoName } = this.props;
return (
<table id={this.aggregateId} className="table-hover">
<tbody onMouseDown={this.onMouseDown}>
@ -210,7 +225,8 @@ export default class PushJobs extends React.Component {
platforms[id].visible &&
<Platform
platform={platforms[id]}
$injector={this.props.$injector}
repoName={repoName}
$injector={$injector}
key={id}
ref={id}
refOrder={i}
@ -226,5 +242,6 @@ export default class PushJobs extends React.Component {
PushJobs.propTypes = {
push: PropTypes.object.isRequired,
repoName: PropTypes.string.isRequired,
$injector: PropTypes.object.isRequired,
};

298
ui/job-view/PushList.jsx Normal file
Просмотреть файл

@ -0,0 +1,298 @@
import React from 'react';
import Push from './Push';
import { findInstance, findSelectedInstance, scrollToElement } from '../helpers/jobHelper';
import PushLoadErrors from './PushLoadErrors';
export default class PushList extends React.Component {
constructor(props) {
super(props);
const { $injector, repoName } = this.props;
this.$rootScope = $injector.get('$rootScope');
this.$location = $injector.get('$location');
this.$timeout = $injector.get('$timeout');
this.thEvents = $injector.get('thEvents');
this.thNotify = $injector.get('thNotify');
this.thPinboard = $injector.get('thPinboard');
this.thJobFilters = $injector.get('thJobFilters');
this.ThResultSetStore = $injector.get('ThResultSetStore');
this.ThResultSetModel = $injector.get('ThResultSetModel');
this.ThJobModel = $injector.get('ThJobModel');
this.ThResultSetStore.addRepository(repoName);
this.getNextPushes = this.getNextPushes.bind(this);
this.updateUrlFromchange = this.updateUrlFromchange.bind(this);
this.clearJobOnClick = this.clearJobOnClick.bind(this);
this.state = {
pushList: [],
loadingPushes: true,
jobsReady: false,
};
// get our first set of resultsets
this.ThResultSetStore.fetchResultSets(
repoName,
this.ThResultSetStore.defaultResultSetCount,
true
);
}
componentWillMount() {
const { repoName } = this.props;
this.pushesLoadedUnlisten = this.$rootScope.$on(this.thEvents.pushesLoaded, () => {
const pushList = this.ThResultSetStore.getResultSetsArray(repoName);
this.$timeout(() => {
this.setState({ pushList, loadingPushes: false });
}, 0);
});
this.jobsLoadedUnlisten = this.$rootScope.$on(this.thEvents.jobsLoaded, () => {
const pushList = this.ThResultSetStore.getResultSetsArray(repoName);
this.$timeout(() => {
this.setState({ pushList, jobsReady: true });
}, 0);
});
this.jobClickUnlisten = this.$rootScope.$on(this.thEvents.jobClick, (ev, job) => {
this.$location.search('selectedJob', job.id);
const { repoName } = this.props;
if (repoName) {
this.ThResultSetStore.setSelectedJob(repoName, job);
}
});
this.clearSelectedJobUnlisten = this.$rootScope.$on(this.thEvents.clearSelectedJob, () => {
this.$location.search('selectedJob', null);
});
this.changeSelectionUnlisten = this.$rootScope.$on(
this.thEvents.changeSelection, (ev, direction, jobNavSelector) => {
this.changeSelectedJob(ev, direction, jobNavSelector);
}
);
this.jobsLoadedUnlisten = this.$rootScope.$on(
this.thEvents.jobsLoaded, () => {
const selectedJobId = parseInt(this.$location.search().selectedJob);
if (selectedJobId) {
this.setSelectedJobFromQueryString(selectedJobId);
}
}
);
}
componentWillUnmount() {
this.pushesLoadedUnlisten();
this.jobsLoadedUnlisten();
this.jobClickUnlisten();
this.clearSelectedJobUnlisten();
this.changeSelectionUnlisten();
this.jobsLoadedUnlisten();
}
getNextPushes(count, keepFilters) {
this.setState({ loadingPushes: true });
const revision = this.$location.search().revision;
if (revision) {
this.$rootScope.skipNextPageReload = true;
this.$location.search('revision', null);
this.$location.search('tochange', revision);
}
this.ThResultSetStore.fetchResultSets(this.props.repoName, count, keepFilters)
.then(this.updateUrlFromchange);
}
/**
* If the URL has a query string param of ``selectedJob`` then select
* that job on load.
*
* If that job isn't in any of the loaded pushes, then throw
* an error and provide a link to load it with the right push.
*/
setSelectedJobFromQueryString(selectedJobId) {
const { repoName } = this.props;
const { urlBasePath } = this.$rootScope;
const jobMap = this.ThResultSetStore.getJobMap(repoName);
const selectedJobEl = jobMap[`${selectedJobId}`];
// select the job in question
if (selectedJobEl) {
this.$rootScope.$emit(this.thEvents.jobClick, selectedJobEl.job_obj);
} else {
// If the ``selectedJob`` was not mapped, then we need to notify
// the user it's not in the range of the current result set list.
this.ThJobModel.get(repoName, selectedJobId).then((job) => {
this.ThResultSetModel.getResultSet(repoName, job.result_set_id).then((push) => {
const url = `${urlBasePath}?repo=${repoName}&revision=${push.data.revision}&selectedJob=${selectedJobId}`;
// the job exists, but isn't in any loaded push.
// provide a message and link to load the right push
this.thNotify.send(`Selected job id: ${selectedJobId} not within current push range.`,
'danger',
{ sticky: true, linkText: 'Load push', url });
});
}, function () {
// the job wasn't found in the db. Either never existed,
// or was expired and deleted.
this.thNotify.send(
`Unable to find job with id ${selectedJobId}`,
'danger',
{ sticky: true });
});
}
}
updateUrlFromchange() {
// since we fetched more pushes, we need to persist the
// push state in the URL.
const rsArray = this.ThResultSetStore.getResultSetsArray(this.props.repoName);
const updatedLastRevision = _.last(rsArray).revision;
if (this.$location.search().fromchange !== updatedLastRevision) {
this.$rootScope.skipNextPageReload = true;
this.$location.search('fromchange', updatedLastRevision);
}
}
changeSelectedJob(ev, direction, jobNavSelector) {
const jobMap = this.ThResultSetStore.getJobMap(this.props.repoName);
// Get the appropriate next index based on the direction and current job
// selection (if any). Must wrap end to end.
const getIndex = direction === 'next' ?
(idx, jobs) => (idx + 1 > jobs.length - 1 ? 0 : idx + 1) :
(idx, jobs) => (idx - 1 < 0 ? jobs.length - 1 : idx - 1);
// TODO: Move from using jquery here to using the ReactJS state tree (bug 1434679)
// to find the next/prev component to select so that setState can be called
// on the component directly.
//
// Filter the list of possible jobs down to ONLY ones in the .th-view-content
// div (excluding pinboard) and then to the specific selector passed
// in. And then to only VISIBLE (not filtered away) jobs. The exception
// is for the .selected-job. If that's not visible, we still want to
// include it, because it is the anchor from which we find
// the next/previous job.
//
// The .selected-job can be invisible, for instance, when filtered to
// unclassified failures only, and you then classify the selected job.
// It's still selected, but no longer visible.
const jobs = $('.th-view-content')
.find(jobNavSelector.selector)
.filter(':visible, .selected-job');
if (jobs.length) {
const selectedEl = jobs.filter('.selected-job').first();
const selIdx = jobs.index(selectedEl);
const idx = getIndex(selIdx, jobs);
const jobEl = $(jobs[idx]);
if (selectedEl.length) {
const selected = findInstance(selectedEl[0]);
selected.setSelected(false);
}
const nextSelected = findInstance(jobEl[0]);
nextSelected.setSelected(true);
const jobId = jobEl.attr('data-job-id');
if (jobMap && jobMap[jobId] && selIdx !== idx) {
this.selectJob(jobMap[jobId].job_obj, jobEl);
return;
}
}
// if there was no new job selected, then ensure that we clear any job that
// was previously selected.
if ($('.selected-job').css('display') === 'none') {
this.$rootScope.closeJob();
}
}
selectJob(job, jobEl) {
// Delay switching jobs right away, in case the user is switching rapidly between jobs
scrollToElement(jobEl);
if (this.jobChangedTimeout) {
window.clearTimeout(this.jobChangedTimeout);
}
this.jobChangedTimeout = window.setTimeout(() => {
this.$rootScope.$emit(this.thEvents.jobClick, job);
}, 200);
}
// Clear the job if it occurs in a particular area
clearJobOnClick(event) {
// Suppress for various UI elements so selection is preserved
const ignoreClear = event.target.hasAttribute("data-ignore-job-clear-on-click");
if (!ignoreClear && !this.thPinboard.hasPinnedJobs()) {
const selected = findSelectedInstance();
if (selected) {
selected.setSelected(false);
}
this.$timeout(this.$rootScope.closeJob);
}
}
render() {
const { $injector, user, repoName, revision } = this.props;
const currentRepo = this.props.currentRepo || {};
const { pushList, loadingPushes, jobsReady } = this.state;
const { loggedin, is_staff } = user;
return (
<div onClick={this.clearJobOnClick}>
{jobsReady && <span className="hidden ready" />}
{repoName && pushList.map(push => (
<Push
push={push}
isTryRepo={currentRepo.is_try_repo}
loggedIn={loggedin || false}
isStaff={is_staff}
repoName={repoName}
$injector={$injector}
key={push.id}
/>
))}
{loadingPushes &&
<div
className="progress active progress-bar progress-bar-striped"
role="progressbar"
/>
}
{pushList.length === 0 && !loadingPushes &&
<PushLoadErrors
loadingPushes={loadingPushes}
currentRepo={currentRepo}
repoName={repoName}
revision={revision}
/>
}
<div className="card card-body get-next">
<span>get next:</span>
<div className="btn-group">
{[10, 20, 50].map(count => (
<div
className="btn btn-light-bordered"
onClick={() => (this.getNextPushes(count, true))}
key={count}
>{count}</div>
))}
</div>
</div>
</div>
);
}
}
treeherder.directive('pushList', ['reactDirective', '$injector',
(reactDirective, $injector) => reactDirective(
PushList,
['repoName', 'user', 'revision', 'currentRepo'],
{},
{ $injector }
)
]);

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

@ -0,0 +1,56 @@
const PushLoadErrors = (props) => {
const { loadingPushes, currentRepo, revision, repoName } = props;
const urlParams = new URLSearchParams(location.hash.split('?')[1]);
urlParams.delete("revision");
return (
<div className="push-load-errors">
{loadingPushes && revision && currentRepo && currentRepo.url &&
<div className="push-body unknown-message-body">
<span>
{revision &&
<span>
<span>Waiting for a push with revision <strong>{revision}</strong></span>
<a
href={currentRepo.getPushLogHref(revision)}
target="_blank"
title={`open revision ${revision} on ${currentRepo.url}`}
>(view pushlog)</a>
<span className="fa fa-spinner fa-pulse th-spinner" />
<div>If the push exists, it will appear in a few minutes once it has been processed.</div>
</span>
}
</span>
</div>
}
{!loadingPushes && revision &&
<div className="push-body unknown-message-body">
This is an invalid or unknown revision. Please change it, or click
<a href={`/#/jobs?${urlParams.toString()}`}> here</a> to reload the latest revisions from {repoName}.
</div>
}
{!loadingPushes && !revision && currentRepo &&
<div className="push-body unknown-message-body">
<span>
<div><b>No pushes found.</b></div>
<span>No commit information could be loaded for this repository.
More information about this repository can be found <a href={currentRepo.url}>here</a>.</span>
</span>
</div>
}
{!loadingPushes && !Object.keys(currentRepo).length &&
<div className="push-body unknown-message-body">
<span>
<div><b>Unknown repository.</b></div>
<span>This repository is either unknown to Treeherder or it does not exist.
If this repository does exist, please <a href="https://bugzilla.mozilla.org/enter_bug.cgi?product=Tree%20Management&component=Treeherder">
file a bug against the Treeherder product in Bugzilla</a> to get it added to the system.
</span>
</span>
</div>
}
</div>
);
};
export default PushLoadErrors;

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

@ -1,194 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { actions, store } from './redux/store';
class Repo extends React.PureComponent {
// While this component doesn't render anything, it acts as a top-level,
// single-point for some events so that they don't happen for each push
// object that is rendered.
// TODO: Once we migrate the ng-repeat to ReactJS for the list of
// pushes, this component will do the rendering of that array.
// This component could arguably be renamed as PushList at that time.
// (bug 1434677).
constructor(props) {
super(props);
this.$rootScope = this.props.$injector.get('$rootScope');
this.$location = this.props.$injector.get('$location');
this.thEvents = this.props.$injector.get('thEvents');
this.ThResultSetStore = this.props.$injector.get('ThResultSetStore');
this.thNotify = this.props.$injector.get('thNotify');
this.thNotify = this.props.$injector.get('thNotify');
this.ThJobModel = this.props.$injector.get('ThJobModel');
this.ThResultSetModel = this.props.$injector.get('ThResultSetModel');
}
componentDidMount() {
this.$rootScope.$on(this.thEvents.jobClick, (ev, job) => {
this.$location.search('selectedJob', job.id);
this.ThResultSetStore.setSelectedJob(this.$rootScope.repoName, job);
store.dispatch(actions.pushes.setSelectedJobId(job.id));
});
this.$rootScope.$on(this.thEvents.clearSelectedJob, () => {
this.$location.search('selectedJob', null);
});
this.$rootScope.$on(
this.thEvents.changeSelection, (ev, direction, jobNavSelector) => {
this.changeSelectedJob(ev, direction, jobNavSelector);
}
);
this.$rootScope.$on(
this.thEvents.jobsLoaded, () => {
const selectedJobId = parseInt(this.$location.search().selectedJob);
if (selectedJobId) {
this.setSelectedJobFromQueryString(selectedJobId);
}
}
);
}
/**
* If the URL has a query string param of ``selectedJob`` then select
* that job on load.
*
* If that job isn't in any of the loaded resultsets, then throw
* an error and provide a link to load it with the right resultset.
*/
setSelectedJobFromQueryString(selectedJobId) {
const jobMap = this.ThResultSetStore.getJobMap(this.$rootScope.repoName);
const selectedJobEl = jobMap[`${selectedJobId}`];
// select the job in question
if (selectedJobEl) {
const jobSelector = `button[data-job-id='${selectedJobId}']`;
this.$rootScope.$emit(this.thEvents.jobClick, selectedJobEl.job_obj);
// scroll to make it visible
this.scrollToElement($('.th-view-content').find(jobSelector).first());
} else {
// If the ``selectedJob`` was not mapped, then we need to notify
// the user it's not in the range of the current result set list.
this.ThJobModel.get(this.$rootScope.repoName, selectedJobId).then((job) => {
this.ThResultSetModel.getResultSet(this.$rootScope.repoName, job.result_set_id).then(function (resultset) {
const url = `${this.$rootScope.urlBasePath}?repo=${this.$rootScope.repoName}&revision=${resultset.data.revision}&selectedJob=${selectedJobId}`;
// the job exists, but isn't in any loaded resultset.
// provide a message and link to load the right resultset
this.thNotify.send(`Selected job id: ${selectedJobId} not within current push range.`,
'danger',
{ sticky: true, linkText: 'Load push', url });
});
}, function () {
// the job wasn't found in the db. Either never existed,
// or was expired and deleted.
this.thNotify.send(`Unable to find job with id ${selectedJobId}`, 'danger', { sticky: true });
});
}
}
changeSelectedJob(ev, direction, jobNavSelector) {
const jobMap = this.ThResultSetStore.getJobMap(this.$rootScope.repoName);
// Get the appropriate next index based on the direction and current job
// selection (if any). Must wrap end to end.
const getIndex = direction === 'next' ?
(idx, jobs) => (idx + 1 > jobs.length - 1 ? 0 : idx + 1) :
(idx, jobs) => (idx - 1 < 0 ? jobs.length - 1 : idx - 1);
// TODO: Move from using jquery here to using the ReactJS state tree (bug 1434679)
// to find the next/prev component to select so that setState can be called
// on the component directly.
//
// Filter the list of possible jobs down to ONLY ones in the .th-view-content
// div (excluding pinboard) and then to the specific selector passed
// in. And then to only VISIBLE (not filtered away) jobs. The exception
// is for the .selected-job. If that's not visible, we still want to
// include it, because it is the anchor from which we find
// the next/previous job.
//
// The .selected-job can be invisible, for instance, when filtered to
// unclassified failures only, and you then classify the selected job.
// It's still selected, but no longer visible.
const jobs = $('.th-view-content').find(jobNavSelector.selector).filter(':visible, .selected-job, .selected-count');
if (jobs.length) {
const selIdx = jobs.index(jobs.filter('.selected-job, .selected-count').first());
const idx = getIndex(selIdx, jobs);
const jobEl = $(jobs[idx]);
const jobId = jobEl.attr('data-job-id');
if (jobMap && jobMap[jobId] && selIdx !== idx) {
this.selectJob(jobMap[jobId].job_obj, jobEl);
return;
}
}
// if there was no new job selected, then ensure that we clear any job that
// was previously selected.
if ($('.selected-job').css('display') === 'none') {
this.$rootScope.closeJob();
}
}
// TODO: see if Element.scrollIntoView() can be used here. (bug 1434679)
scrollToElement(el, duration) {
if (_.isUndefined(duration)) {
duration = 50;
}
if (el.position() !== undefined) {
var scrollOffset = -50;
if (window.innerHeight >= 500 && window.innerHeight < 1000) {
scrollOffset = -100;
} else if (window.innerHeight >= 1000) {
scrollOffset = -200;
}
if (!this.isOnScreen(el)) {
$('.th-global-content').scrollTo(el, duration, { offset: scrollOffset });
}
}
}
isOnScreen(el) {
const viewport = {};
viewport.top = $(window).scrollTop() + $('#global-navbar-container').height() + 30;
const filterbarheight = $('.active-filters-bar').height();
viewport.top = filterbarheight > 0 ? viewport.top + filterbarheight : viewport.top;
const updatebarheight = $('.update-alert-panel').height();
viewport.top = updatebarheight > 0 ? viewport.top + updatebarheight : viewport.top;
viewport.bottom = $(window).height() - $('#info-panel').height() - 20;
const bounds = {};
bounds.top = el.offset().top;
bounds.bottom = bounds.top + el.outerHeight();
return ((bounds.top <= viewport.bottom) && (bounds.bottom >= viewport.top));
}
selectJob(job, jobEl) {
// Delay switching jobs right away, in case the user is switching rapidly between jobs
store.dispatch(actions.pushes.setSelectedJobId(job.id));
this.scrollToElement(jobEl);
if (this.jobChangedTimeout) {
window.clearTimeout(this.jobChangedTimeout);
}
this.jobChangedTimeout = window.setTimeout(() => {
this.$rootScope.$emit(this.thEvents.jobClick, job);
}, 200);
}
render() {
return null;
}
}
Repo.propTypes = {
$injector: PropTypes.object.isRequired,
};
// Need store here because this React component is not wrapped in a <Provider>.
// Once we're completely on React, the entire app will be wrapped in a singular
// <Provider> so all components will get the store.
treeherder.constant('store', store);
treeherder.directive('repo', ['reactDirective', '$injector', (reactDirective, $injector) =>
reactDirective(Repo, ['repo'], {}, { $injector })]);

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

@ -1,4 +1,5 @@
import PropTypes from 'prop-types';
import { parseAuthor } from '../helpers/revisionHelper';
export const Initials = (props) => {
const str = props.author || '';
@ -27,34 +28,31 @@ export const Initials = (props) => {
export class Revision extends React.PureComponent {
constructor(props) {
super(props);
this.userTokens = this.props.revision.author.split(/[<>]+/);
this.name = this.userTokens[0].trim().replace(/\w\S*/g,
txt => txt.charAt(0).toUpperCase() + txt.substr(1));
if (this.userTokens.length > 1) this.email = this.userTokens[1];
const comment = this.props.revision.comments.split('\n')[0];
const escapedComment = _.escape(comment);
this.escapedCommentHTML = { __html: this.props.linkifyBugsFilter(escapedComment) };
const { revision, linkifyBugsFilter } = this.props;
this.tags = '';
if (escapedComment.search('Backed out') >= 0 ||
escapedComment.search('Back out') >= 0) {
this.tags += 'backout';
}
const escapedComment = _.escape(revision.comments.split('\n')[0]);
this.escapedCommentHTML = { __html: linkifyBugsFilter(escapedComment) };
this.tags = escapedComment.search('Backed out') >= 0 || escapedComment.search('Back out') >= 0 ?
'backout' : '';
}
render() {
const { revision, repo } = this.props;
const { name, email } = parseAuthor(revision.author);
const commitRevision = revision.revision;
return (<li className="clearfix">
<span className="revision" data-tags={this.tags}>
<span className="revision-holder">
<a
title={`Open revision ${this.props.revision.revision} on ${this.props.repo.url}`}
href={this.props.repo.getRevisionHref(this.props.revision.revision)}
title={`Open revision ${commitRevision} on ${repo.url}`}
href={repo.getRevisionHref(commitRevision)}
data-ignore-job-clear-on-click
>{this.props.revision.revision.substring(0, 12)}
>{commitRevision.substring(0, 12)}
</a>
</span>
<Initials title={`${this.name}: ${this.email}`}
author={this.name}
<Initials title={`${name}: ${email}`}
author={name}
/>
<span title={this.comment}>
<span className="revision-comment">

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

@ -9,21 +9,23 @@ export class RevisionList extends React.PureComponent {
}
render() {
const { push, repo } = this.props;
return (
<span className="revision-list col-5">
<ul className="list-unstyled">
{this.props.push.revisions.map((revision, i) =>
{push.revisions.map((revision, i) =>
(<Revision
linkifyBugsFilter={this.linkifyBugsFilter}
revision={revision}
repo={this.props.repo}
repo={repo}
key={i}
/>)
)}
{this.hasMore &&
<MoreRevisionsLink
key="more"
href={this.props.repo.getPushLogHref(this.props.push.revision)}
href={repo.getPushLogHref(push.revision)}
/>
}
</ul>
@ -43,9 +45,7 @@ export const MoreRevisionsLink = props => (
<a href={props.href}
data-ignore-job-clear-on-click
target="_blank"
>{`\u2026and more`}
<i className="fa fa-external-link-square" />
</a>
>{`\u2026and more`}<i className="fa fa-external-link-square" /></a>
</li>
);

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

@ -1,20 +1,17 @@
export const escape = function (id) {
return id.replace(/(:|\[|\]|\?|,|\.|\s+)/g, '-');
};
export const escape = id => (
id.replace(/(:|\[|\]|\?|,|\.|\s+)/g, '-')
);
export const getPlatformRowId = function (repoName, resultsetId, platformName, platformOptions) {
export const getPlatformRowId = (repoName, pushId, platformName, platformOptions) => (
// ensure there are no invalid characters in the id (like spaces, etc)
return escape(repoName +
resultsetId +
platformName +
platformOptions);
};
escape(repoName + pushId + platformName + platformOptions)
);
export const getResultsetTableId = function (repoName, resultsetId, revision) {
return escape(repoName + resultsetId + revision);
};
export const getPushTableId = (repoName, pushId, revision) => (
escape(repoName + pushId + revision)
);
export const getGroupMapKey = function (result_set_id, grSymbol, grTier, plName, plOpt) {
export const getGroupMapKey = (pushId, grSymbol, grTier, plName, plOpt) => (
//Build string key for groupMap entries
return escape(result_set_id + grSymbol + grTier + plName + plOpt);
};
escape(pushId + grSymbol + grTier + plName + plOpt)
);

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

@ -1,14 +0,0 @@
import { createStore, bindActionCreators, combineReducers } from 'redux';
import * as pushes from './modules/pushes';
export default () => {
const reducer = combineReducers({
pushes: pushes.reducer,
});
const store = createStore(reducer);
const actions = {
pushes: bindActionCreators(pushes.actions, store.dispatch),
};
return { store, actions };
};

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

@ -1,60 +0,0 @@
export const types = {
SET_COUNTS_EXPANDED: 'SET_COUNTS_EXPANDED',
SET_SHOW_DUPLICATES: 'SET_SHOW_DUPLICATES',
SET_SELECTED_JOB: 'SET_SELECTED_JOB',
SET_SELECTED_RUNNABLE_JOBS: 'SET_SELECTED_RUNNABLE_JOBS',
};
export const actions = {
setSelectedJobId: jobId => ({
type: types.SET_SELECTED_JOB,
payload: {
jobId,
}
}),
setSelectedRunnableJobs: selectedRunnableJobs => ({
type: types.SET_SELECTED_RUNNABLE_JOBS,
payload: {
selectedRunnableJobs,
}
}),
setCountExpanded: countsExpanded => ({
type: types.SET_COUNTS_EXPANDED,
payload: {
countsExpanded
}
}),
setShowDuplicates: showDuplicates => ({
type: types.SET_SHOW_DUPLICATES,
payload: {
showDuplicates
}
}),
};
const initialState = {
selectedJobId: null,
selectedRunnableJobs: null,
countsExpanded: false,
showDuplicates: false,
};
export const reducer = (state = initialState, action) => {
switch (action.type) {
case types.SET_SELECTED_JOB:
return {
...state,
selectedJobId: action.payload.jobId
};
case types.SET_SELECTED_RUNNABLE_JOBS:
case types.SET_SHOW_DUPLICATES:
case types.SET_COUNTS_EXPANDED:
return {
...state,
...action.payload
};
default:
return state;
}
};

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

@ -1,3 +0,0 @@
import configureStore from './configureStore';
export const { store, actions } = configureStore();

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

@ -1,272 +0,0 @@
treeherderApp.controller('JobsCtrl', [
'$scope', '$rootScope', '$routeParams',
'thDefaultRepo',
'ThResultSetStore', '$location', 'thEvents',
'thNotify',
function JobsCtrl(
$scope, $rootScope, $routeParams,
thDefaultRepo,
ThResultSetStore, $location, thEvents, thNotify) {
// load our initial set of resultsets
// scope needs this function so it can be called directly by the user, too.
$scope.getNextResultSets = function (count, keepFilters) {
var revision = $location.search().revision;
if (revision) {
$rootScope.skipNextPageReload = true;
$location.search('revision', null);
$location.search('tochange', revision);
}
ThResultSetStore.fetchResultSets($scope.repoName, count, keepFilters)
.then(function () {
// since we fetched more resultsets, we need to persist the
// resultset state in the URL.
var rsArray = ThResultSetStore.getResultSetsArray($scope.repoName);
var updatedLastRevision = _.last(rsArray).revision;
if ($location.search().fromchange !== updatedLastRevision) {
$rootScope.skipNextPageReload = true;
$location.search('fromchange', updatedLastRevision);
}
});
};
// set to the default repo if one not specified
if ($routeParams.hasOwnProperty("repo") &&
$routeParams.repo !== "") {
$rootScope.repoName = $routeParams.repo;
} else {
$rootScope.repoName = thDefaultRepo;
$location.search("repo", $rootScope.repoName);
}
ThResultSetStore.addRepository($scope.repoName);
$scope.isLoadingRsBatch = ThResultSetStore.getLoadingStatus($scope.repoName);
$scope.result_sets = ThResultSetStore.getResultSetsArray($scope.repoName);
$scope.job_map = ThResultSetStore.getJobMap($scope.repoName);
$scope.searchParams = $location.search();
$scope.locationHasSearchParam = function (prop) {
return _.has($scope.searchParams, prop);
};
$scope.getSearchParamValue = function (param) {
var params = $location.search();
var searchParamValue = params[param];
// in the event the user manually strips off the search
// parameter and its = sign, which would return true
if (searchParamValue === true) {
return "";
}
return searchParamValue;
};
if ($location.search().revision === 'undefined') {
thNotify.send("Invalid value for revision parameter.", 'danger');
}
if (ThResultSetStore.isNotLoaded($scope.repoName)) {
// get our first set of resultsets
ThResultSetStore.fetchResultSets(
$scope.repoName,
ThResultSetStore.defaultResultSetCount,
true);
}
}
]);
treeherderApp.controller('ResultSetCtrl', [
'$scope', '$rootScope',
'thResultStatusInfo', 'thDateFormat',
'ThResultSetStore', 'thEvents', 'thNotify',
'thBuildApi', 'thPinboard', 'ThResultSetModel', 'dateFilter',
'ThModelErrors', 'ThTaskclusterErrors', '$uibModal', 'thPinboardCountError',
function ResultSetCtrl(
$scope, $rootScope,
thResultStatusInfo, thDateFormat,
ThResultSetStore, thEvents, thNotify,
thBuildApi, thPinboard, ThResultSetModel, dateFilter, ThModelErrors,
ThTaskclusterErrors, $uibModal, thPinboardCountError) {
$scope.getCountClass = function (resultStatus) {
return thResultStatusInfo(resultStatus).btnClass;
};
$scope.getCountText = function (resultStatus) {
return thResultStatusInfo(resultStatus).countText;
};
$scope.viewJob = function (job) {
// set the selected job
$rootScope.selectedJob = job;
};
/**
* Pin all jobs that pass the global filters.
*
* If optional resultsetId is passed in, then only pin jobs from that
* resultset.
*/
$scope.pinAllShownJobs = function () {
if (!thPinboard.spaceRemaining()) {
thNotify.send(thPinboardCountError, 'danger');
return;
}
var shownJobs = ThResultSetStore.getAllShownJobs(
$rootScope.repoName,
thPinboard.spaceRemaining(),
thPinboardCountError,
$scope.resultset.id
);
thPinboard.pinJobs(shownJobs);
if (!$rootScope.selectedJob) {
$scope.viewJob(shownJobs[0]);
}
};
$scope.showRunnableJobs = function () {
if ($scope.user.loggedin) {
$rootScope.$emit(thEvents.showRunnableJobs, $scope.resultset);
}
};
$scope.deleteRunnableJobs = function () {
$rootScope.$emit(thEvents.deleteRunnableJobs, $scope.resultset);
};
$scope.getCancelJobsTitle = function () {
if (!$scope.user || !$scope.user.loggedin) {
return "Must be logged in to cancel jobs";
}
return "Cancel all jobs";
};
$scope.canCancelJobs = function () {
return $scope.user && $scope.user.loggedin;
};
$scope.confirmCancelAllJobs = function () {
$scope.showConfirmCancelAll = true;
};
$scope.hideConfirmCancelAll = function () {
$scope.showConfirmCancelAll = false;
};
$scope.cancelAllJobs = function (revision) {
$scope.showConfirmCancelAll = false;
if (!$scope.canCancelJobs()) return;
ThResultSetModel.cancelAll($scope.resultset.id, $scope.repoName).then(function () {
return thBuildApi.cancelAll($scope.repoName, revision);
}).catch(function (e) {
thNotify.send(
ThModelErrors.format(e, "Failed to cancel all jobs"),
'danger', true
);
});
};
$scope.customPushAction = function () {
$uibModal.open({
templateUrl: 'partials/main/tcjobactions.html',
controller: 'TCJobActionsCtrl',
size: 'lg',
resolve: {
job: () => null,
repoName: function () {
return $scope.repoName;
},
resultsetId: function () {
return $scope.resultset.id;
}
}
});
};
$scope.triggerMissingJobs = function (revision) {
if (!window.confirm('This will trigger all missing jobs for revision ' + revision + '!\n\nClick "OK" if you want to proceed.')) {
return;
}
ThResultSetStore.getGeckoDecisionTaskId(
$scope.repoName,
$scope.resultset.id
).then(function (decisionTaskID) {
ThResultSetModel.triggerMissingJobs(
decisionTaskID
).then(function (msg) {
thNotify.send(msg, "success");
}, function (e) {
thNotify.send(
ThModelErrors.format(e, "The action 'trigger missing jobs' failed"),
'danger', true
);
});
});
};
$scope.triggerAllTalosJobs = function (revision) {
if (!window.confirm('This will trigger all talos jobs for revision ' + revision + '!\n\nClick "OK" if you want to proceed.')) {
return;
}
var times = parseInt(window.prompt("Enter number of instances to have for each talos job", 6));
while (times < 1 || times > 6 || isNaN(times)) {
times = window.prompt("We only allow instances of each talos job to be between 1 to 6 times. Enter again", 6);
}
ThResultSetStore.getGeckoDecisionTaskId(
$scope.repoName,
$scope.resultset.id
).then(function (decisionTaskID) {
ThResultSetModel.triggerAllTalosJobs(
times,
decisionTaskID
).then(function (msg) {
thNotify.send(msg, "success");
}, function (e) {
thNotify.send(ThTaskclusterErrors.format(e), 'danger', { sticky: true });
});
});
};
$scope.showTriggerButton = function () {
var buildernames = ThResultSetStore.getSelectedRunnableJobs($rootScope.repoName, $scope.resultset.id);
return buildernames.length > 0;
};
$scope.triggerNewJobs = function () {
if (!window.confirm(
'This will trigger all selected jobs. Click "OK" if you want to proceed.')) {
return;
}
if ($scope.user.loggedin) {
var buildernames = ThResultSetStore.getSelectedRunnableJobs($rootScope.repoName, $scope.resultset.id);
ThResultSetStore.getGeckoDecisionTaskId($rootScope.repoName, $scope.resultset.id).then(function (decisionTaskID) {
ThResultSetModel.triggerNewJobs(buildernames, decisionTaskID).then(function (result) {
thNotify.send(result, "success");
ThResultSetStore.deleteRunnableJobs($scope.repoName, $scope.resultset);
}, function (e) {
thNotify.send(ThTaskclusterErrors.format(e), 'danger', { sticky: true });
});
});
} else {
thNotify.send("Must be logged in to trigger a job", 'danger');
}
};
$scope.revisionResultsetFilterUrl = $scope.urlBasePath + "?repo=" +
$scope.repoName + "&revision=" +
$scope.resultset.revision;
$scope.resultsetDateStr = dateFilter($scope.resultset.push_timestamp*1000,
thDateFormat);
$scope.authorResultsetFilterUrl = $scope.urlBasePath + "?repo=" +
$scope.repoName + "&author=" +
encodeURIComponent($scope.resultset.author);
}
]);

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

@ -26,6 +26,18 @@ treeherderApp.controller('MainCtrl', [
// Ensure user is available on initial page load
$rootScope.user = {};
// set to the default repo if one not specified
const repoName = $location.search().repo;
if (repoName) {
$rootScope.repoName = repoName;
} else {
$rootScope.repoName = thDefaultRepo;
$location.search("repo", $rootScope.repoName);
}
$rootScope.revision = $location.search().revision;
ThResultSetStore.addRepository($rootScope.repoName);
thClassificationTypes.load();
var checkServerRevision = function () {
@ -41,9 +53,6 @@ treeherderApp.controller('MainCtrl', [
});
};
// Trigger missing jobs is dangerous on repos other than these (see bug 1335506)
$scope.triggerMissingRepos = ['mozilla-inbound', 'autoland'];
$scope.updateButtonClick = function () {
if (window.confirm("Reload the page to pick up Treeherder updates?")) {
window.location.reload(true);
@ -138,23 +147,12 @@ treeherderApp.controller('MainCtrl', [
$rootScope.selectedJob = null;
// Clear the selected job display style
$rootScope.$emit(thEvents.clearSelectedJob, $rootScope.selectedJob);
$rootScope.$emit(thEvents.clearSelectedJob);
// Reset selected job to null to initialize nav position
ThResultSetStore.setSelectedJob($rootScope.repoName);
};
// Clear the job if it occurs in a particular area
$scope.clearJobOnClick = function (event) {
var element = event.target;
// Suppress for various UI elements so selection is preserved
var ignoreClear = element.hasAttribute("data-ignore-job-clear-on-click");
if (!ignoreClear && !thPinboard.hasPinnedJobs()) {
$scope.closeJob();
}
};
$scope.repoModel = ThRepositoryModel;
/**

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

@ -1,31 +1,31 @@
treeherder.directive('thPinnedJob', [
'thResultStatusInfo', 'thResultStatus',
function (thResultStatusInfo, thResultStatus) {
import { getBtnClass, getStatus } from "../../../helpers/jobHelper";
var getHoverText = function (job) {
var duration = Math.round((job.end_timestamp - job.start_timestamp) / 60);
var status = thResultStatus(job);
return job.job_type_name + " - " + status + " - " + duration + "mins";
};
treeherder.directive('thPinnedJob', function () {
return {
restrict: "E",
link: function (scope) {
var unbindWatcher = scope.$watch("job", function () {
var resultState = thResultStatus(scope.job);
scope.job.display = thResultStatusInfo(resultState, scope.job.failure_classification_id);
scope.hoverText = getHoverText(scope.job);
var getHoverText = function (job) {
var duration = Math.round((job.end_timestamp - job.start_timestamp) / 60);
var status = getStatus(job);
return job.job_type_name + " - " + status + " - " + duration + "mins";
};
if (scope.job.state === "completed") {
//Remove watchers when a job has a completed status
unbindWatcher();
}
return {
restrict: "E",
link: function (scope) {
var unbindWatcher = scope.$watch("job", function () {
var resultState = getStatus(scope.job);
scope.job.btnClass = getBtnClass(resultState, scope.job.failure_classification_id);
scope.hoverText = getHoverText(scope.job);
}, true);
},
templateUrl: 'partials/main/thPinnedJob.html'
};
}]);
if (scope.job.state === "completed") {
//Remove watchers when a job has a completed status
unbindWatcher();
}
}, true);
},
templateUrl: 'partials/main/thPinnedJob.html'
};
});
treeherder.directive('thRelatedBugQueued', function () {

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

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

@ -1,48 +0,0 @@
treeherder.directive('thActionButton', function () {
return {
restrict: "E",
templateUrl: 'partials/main/thActionButton.html'
};
});
treeherder.directive('thResultCounts', [
'thEvents', '$rootScope', function (thEvents, $rootScope) {
return {
restrict: "E",
link: function (scope) {
var setTotalCount = function () {
if (scope.resultset.job_counts) {
scope.inProgress = scope.resultset.job_counts.pending +
scope.resultset.job_counts.running;
var total = scope.resultset.job_counts.completed + scope.inProgress;
scope.percentComplete = total > 0 ?
Math.floor(((scope.resultset.job_counts.completed / total) * 100)) : undefined;
}
};
$rootScope.$on(thEvents.applyNewJobs, function (evt, resultSetId) {
if (resultSetId === scope.resultset.id) {
setTotalCount();
}
});
},
templateUrl: 'partials/main/thResultCounts.html'
};
}]);
treeherder.directive('thAuthor', function () {
return {
restrict: "E",
link: function (scope, element, attrs) {
var authorMatch = attrs.author.match(/\<(.*?)\>+/);
scope.authorEmail = authorMatch ? authorMatch[1] : attrs.author;
},
template: '<span title="View pushes by this user">' +
'<a href="{{authorResultsetFilterUrl}}"' +
'data-ignore-job-clear-on-click>{{authorEmail}}</a></span>'
};
});

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

@ -1,3 +1,5 @@
import { getBtnClass } from '../../../helpers/jobHelper';
treeherder.directive('thWatchedRepo', [
'ThLog', 'ThRepositoryModel',
function (ThLog, ThRepositoryModel) {
@ -152,15 +154,12 @@ treeherder.directive('thRepoMenuItem',
};
});
treeherder.directive('thResultStatusChicklet', [
'thResultStatusInfo', function (thResultStatusInfo) {
return {
restrict: "E",
link: function (scope) {
scope.chickletClass = thResultStatusInfo(scope.filterName).btnClass +
"-filter-chicklet";
},
templateUrl: 'partials/main/thResultStatusChicklet.html'
};
}
]);
treeherder.directive('thResultStatusChicklet', function () {
return {
restrict: "E",
link: function (scope) {
scope.chickletClass = `${getBtnClass(scope.filterName)}-filter-chicklet`;
},
templateUrl: 'partials/main/thResultStatusChicklet.html'
};
});

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

@ -348,9 +348,11 @@ treeherder.factory('ThResultSetStore', [
});
};
var deleteRunnableJobs = function (repoName, resultSet) {
repositories[repoName].rsMap[resultSet.id].selected_runnable_jobs = [];
resultSet.isRunnableVisible = false;
var deleteRunnableJobs = function (repoName, pushId) {
const push = repositories[repoName].rsMap[pushId];
push.selected_runnable_jobs = [];
push.rs_obj.isRunnableVisible = false;
$rootScope.$emit(thEvents.selectRunnableJob, []);
$rootScope.$emit(thEvents.globalFilterChanged);
};
@ -671,7 +673,7 @@ treeherder.factory('ThResultSetStore', [
revision = repositories[repoName].rsMap[resultsetId].rs_obj.revision;
resultsetAggregateId = thAggregateIds.getResultsetTableId(
resultsetAggregateId = thAggregateIds.getPushTableId(
$rootScope.repoName, resultsetId, revision
);
@ -806,21 +808,18 @@ treeherder.factory('ThResultSetStore', [
isInResultSetRange(repoName, data.results[i].push_timestamp) &&
repositories[repoName].rsMap[data.results[i].id] === undefined) {
$log.debug("prepending resultset: ", data.results[i].id);
repositories[repoName].resultSets.push(data.results[i]);
added.push(data.results[i]);
} else {
$log.debug("not prepending. timestamp is older");
}
}
mapResultSets(repoName, added);
repositories[repoName].loadingStatus.prepending = false;
$rootScope.$emit(thEvents.pushesLoaded);
};
var appendResultSets = function (repoName, data) {
if (data.results.length > 0) {
$log.debug("appendResultSets", data.results);
@ -850,6 +849,7 @@ treeherder.factory('ThResultSetStore', [
}
repositories[repoName].loadingStatus.appending = false;
$rootScope.$emit(thEvents.pushesLoaded);
};
/**
@ -883,19 +883,18 @@ treeherder.factory('ThResultSetStore', [
return repositories[repoName].resultSets;
};
var getResultSetsMap = function (repoName) {
return repositories[repoName].rsMap;
};
var getResultSet = function (repoName, resultsetId) {
return repositories[repoName].rsMap[resultsetId].rs_obj;
};
var getSelectedRunnableJobs = function (repoName, resultsetId) {
if (!repositories[repoName].rsMap[resultsetId].selected_runnable_jobs) {
repositories[repoName].rsMap[resultsetId].selected_runnable_jobs = [];
var getSelectedRunnableJobs = function (repoName, pushId) {
if (!repositories[repoName].rsMap[pushId]) {
return 0;
}
return repositories[repoName].rsMap[resultsetId].selected_runnable_jobs;
if (!repositories[repoName].rsMap[pushId].selected_runnable_jobs) {
repositories[repoName].rsMap[pushId].selected_runnable_jobs = [];
}
return repositories[repoName].rsMap[pushId].selected_runnable_jobs;
};
var getGeckoDecisionJob = function (repoName, resultsetId) {
@ -948,27 +947,14 @@ treeherder.factory('ThResultSetStore', [
} else {
selectedRunnableJobs.splice(jobIndex, 1);
}
$rootScope.$emit(thEvents.selectRunnableJob, selectedRunnableJobs, resultsetId);
return selectedRunnableJobs;
};
var isRunnableJobSelected = function (repoName, resultsetId, buildername) {
var selectedRunnableJobs = getSelectedRunnableJobs(repoName, resultsetId);
return selectedRunnableJobs.indexOf(buildername) !== -1;
};
var getJobMap = function (repoName) {
// this is a "watchable" for jobs
return repositories[repoName].jobMap;
};
var getGroupMap = function (repoName) {
return repositories[repoName].grpMap;
};
var getLoadingStatus = function (repoName) {
return repositories[repoName].loadingStatus;
};
var isNotLoaded = function (repoName) {
return _.isEmpty(repositories[repoName].rsMap);
};
var fetchResultSets = function (repoName, count, keepFilters) {
/**
@ -1184,22 +1170,15 @@ treeherder.factory('ThResultSetStore', [
fetchResultSets: fetchResultSets,
getAllShownJobs: getAllShownJobs,
getJobMap: getJobMap,
getGroupMap: getGroupMap,
getLoadingStatus: getLoadingStatus,
getPlatformKey: getPlatformKey,
addRunnableJobs: addRunnableJobs,
isRunnableJobSelected: isRunnableJobSelected,
getSelectedRunnableJobs: getSelectedRunnableJobs,
getGeckoDecisionJob: getGeckoDecisionJob,
getGeckoDecisionTaskId: getGeckoDecisionTaskId,
toggleSelectedRunnableJob: toggleSelectedRunnableJob,
getResultSet: getResultSet,
getResultSetsArray: getResultSetsArray,
getResultSetsMap: getResultSetsMap,
getSelectedJob: getSelectedJob,
getFilteredUnclassifiedFailureCount: getFilteredUnclassifiedFailureCount,
getAllUnclassifiedFailureCount: getAllUnclassifiedFailureCount,
isNotLoaded: isNotLoaded,
setSelectedJob: setSelectedJob,
updateUnclassifiedFailureMap: updateUnclassifiedFailureMap,
defaultResultSetCount: defaultResultSetCount,

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

@ -25,17 +25,6 @@ treeherder.provider('thResultStatusList', function () {
};
});
treeherder.provider('thResultStatus', function () {
this.$get = function () {
return function (job) {
if (job.state === "completed") {
return job.result;
}
return job.state;
};
};
});
treeherder.provider('thResultStatusObject', function () {
var getResultStatusObject = function () {
return {
@ -52,95 +41,6 @@ treeherder.provider('thResultStatusObject', function () {
};
});
treeherder.provider('thResultStatusInfo', function () {
this.$get = function () {
return function (resultState, failure_classification_id) {
// default if there is no match, used for pending
var resultStatusInfo = {
btnClass: "btn-default"
};
switch (resultState) {
case "busted":
case "failures":
resultStatusInfo = {
btnClass: "btn-red",
countText: "busted"
};
break;
case "exception":
resultStatusInfo = {
btnClass: "btn-purple",
countText: "exception"
};
break;
case "testfailed":
resultStatusInfo = {
btnClass: "btn-orange",
countText: "failed"
};
break;
case "unknown":
resultStatusInfo = {
btnClass: "btn-yellow",
countText: "unknown"
};
break;
case "usercancel":
resultStatusInfo = {
btnClass: "btn-pink",
countText: "cancel"
};
break;
case "retry":
resultStatusInfo = {
btnClass: "btn-dkblue",
countText: "retry"
};
break;
case "success":
resultStatusInfo = {
btnClass: "btn-green",
countText: "success"
};
break;
case "running":
case "in progress":
resultStatusInfo = {
btnClass: "btn-dkgray",
countText: "running"
};
break;
case "pending":
resultStatusInfo = {
btnClass: "btn-ltgray",
countText: "pending"
};
break;
case "superseded":
resultStatusInfo = {
btnClass: "btn-ltblue",
countText: "superseded"
};
break;
}
// handle if a job is classified
var classificationId = parseInt(failure_classification_id, 10);
if (classificationId > 1) {
resultStatusInfo.btnClass += "-classified";
// autoclassification-only case
if (classificationId === 7) {
resultStatusInfo.btnClass += " autoclassified";
}
resultStatusInfo.countText = "classified " + resultStatusInfo.countText;
}
return resultStatusInfo;
};
};
});
/**
* The set of custom Treeherder events.
*
@ -183,6 +83,9 @@ treeherder.provider('thEvents', function () {
// after loading a group of jobs
jobsLoaded: "jobs-loaded-EVT",
// when new pushes are prepended, or appended
pushesLoaded: "pushes-loaded-EVT",
// after deselecting a job via click outside/esc
clearSelectedJob: "clear-selected-job-EVT",
@ -239,8 +142,9 @@ treeherder.provider('thEvents', function () {
autoclassifyToggleExpandOptions: "ac-toggle-expand-options-EVT",
autoclassifyToggleEdit: "ac-toggle-edit-EVT"
autoclassifyToggleEdit: "ac-toggle-edit-EVT",
selectRunnableJob: "select-runnable-job-EVT",
};
};
});
@ -249,7 +153,7 @@ treeherder.provider('thAggregateIds', function () {
this.$get = function () {
return {
getPlatformRowId: aggregateIds.getPlatformRowId,
getResultsetTableId: aggregateIds.getResultsetTableId,
getPushTableId: aggregateIds.getPushTableId,
getGroupMapKey: aggregateIds.getGroupMapKey,
escape: aggregateIds.escape
};

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

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

@ -1,3 +1,4 @@
import { getStatus } from '../../helpers/jobHelper';
/**
This service handles whether or not a job, job group or platform row should
be displayed based on the filter settings.
@ -18,13 +19,13 @@
*/
treeherder.factory('thJobFilters', [
'thResultStatusList', 'ThLog', '$rootScope', '$location',
'thEvents', 'thFailureResults',
'thResultStatus', 'thClassificationTypes',
'thEvents', 'thFailureResults', '$timeout',
'thClassificationTypes',
'thPlatformName',
function (
thResultStatusList, ThLog, $rootScope, $location,
thEvents, thFailureResults,
thResultStatus, thClassificationTypes,
thEvents, thFailureResults, $timeout,
thClassificationTypes,
thPlatformName) {
const $log = new ThLog("thJobFilters");
@ -193,9 +194,10 @@ treeherder.factory('thJobFilters', [
function showJob(job) {
// when runnable jobs have been added to a resultset, they should be
// shown regardless of settings for classified or result state
if (job.result !== "runnable") {
const status = getStatus(job);
if (status !== "runnable") {
// test against resultStatus and classifiedState
if (cachedResultStatusFilters.indexOf(thResultStatus(job)) === -1) {
if (cachedResultStatusFilters.indexOf(status) === -1) {
return false;
}
if (!_checkClassifiedStateFilters(job)) {
@ -281,7 +283,9 @@ treeherder.factory('thJobFilters', [
newQsVal = null;
}
$log.debug("add set " + _withPrefix(field) + " from " + oldQsVal + " to " + newQsVal);
$location.search(_withPrefix(field), newQsVal);
$timeout(() => {
$location.search(_withPrefix(field), newQsVal);
}, 0);
}
function removeFilter(field, value) {

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

@ -41,11 +41,11 @@ treeherder.factory('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
* ``local.conf.js`` file. You can see ONLY ``foo`` lines by adding
* a line like:
*
* ThLogConfigProvider.setWhitelist([
* 'ResultSetCtrl'
* 'foo'
* ]);
*
* Note: even though this is called ThLogConfig, when you configure it, you must

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

@ -198,6 +198,32 @@ treeherder.factory('Ajv', [
return require('ajv');
}]);
// The Custom Actions modal is accessible from both the PushActionMenu and the
// job-details-actionbar. So leave this as Angular for now, till we
// migrate job-details-actionbar to React.
treeherder.factory('customPushActions', [
'$uibModal',
function ($uibModal) {
return {
open(repoName, pushId) {
$uibModal.open({
templateUrl: 'partials/main/tcjobactions.html',
controller: 'TCJobActionsCtrl',
size: 'lg',
resolve: {
job: () => null,
repoName: function () {
return repoName;
},
resultsetId: function () {
return pushId;
}
}
});
}
};
}]);
treeherder.factory('jsonSchemaDefaults', [
function () {
return require('json-schema-defaults');

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

@ -32,15 +32,11 @@ treeherderApp.config(['$compileProvider', '$locationProvider', '$routeProvider',
$routeProvider
.when('/jobs', {
controller: 'JobsCtrl',
templateUrl: 'partials/main/jobs.html',
// see controllers/filters.js ``skipNextSearchChangeReload`` for
// why we set this to false.
reloadOnSearch: false
})
.when('/jobs/:tree', {
controller: 'JobsCtrl',
templateUrl: 'partials/main/jobs.html',
reloadOnSearch: false
})
.otherwise({ redirectTo: '/jobs' });

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

@ -159,12 +159,11 @@ treeherder.value("thJobNavSelectors",
{
ALL_JOBS: {
name: "jobs",
selector: ".job-btn, .selected-job, .selected-count"
selector: ".job-btn, .selected-job"
},
UNCLASSIFIED_FAILURES: {
name: "unclassified failures",
selector: ".selected-job, " +
".selected-count, " +
".job-btn.btn-red, " +
".job-btn.btn-orange, " +
".job-btn.btn-purple, " +

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

@ -1,131 +0,0 @@
<!-- Load progress bar -->
<span class="hidden" ng-class="{'ready':jobsReady}"></span>
<div class="progress active"
ng-show="isLoadingRsBatch.prepending && result_sets.length === 0">
<div class="progress-bar progress-bar-striped" role="progressbar" style="width: 100%"></div>
</div>
<repo />
<!-- Main resultset template -->
<div ng-repeat="resultset in result_sets | orderBy:'-push_timestamp' track by resultset.id"
ng-controller="ResultSetCtrl"
class="result-set"
data-id="{{::resultset.id}}">
<div class="result-set-bar">
<span class="result-set-left">
<span class="result-set-title-left">
<span>
<a ng-href="{{::revisionResultsetFilterUrl}}{{filterParams()}}"
title="View only this push"
data-ignore-job-clear-on-click>{{::resultsetDateStr}}
<span class="fa fa-external-link icon-superscript"></span></a> - </span>
<th-author author="{{::resultset.author}}"></th-author>
</span>
</span>
<th-result-counts class="result-counts"></th-result-counts>
<span class="result-set-buttons">
<a class="btn btn-sm btn-resultset test-view-btn"
href="/testview.html?repo={{repoName}}&revision={{resultset.revision}}"
target="_blank"
title="View details on failed test results for this push">View Tests</a>
<button class="btn btn-sm btn-resultset cancel-all-jobs-btn"
ng-attr-title="{{getCancelJobsTitle()}}"
ng-show="currentRepo.is_try_repo || user.is_staff"
data-ignore-job-clear-on-click
ng-disabled="!canCancelJobs()"
ng-click="confirmCancelAllJobs()">
<span class="fa fa-times-circle cancel-job-icon dim-quarter"
data-ignore-job-clear-on-click></span>
</button>
<button class="btn btn-sm btn-resultset pin-all-jobs-btn"
title="Pin all available jobs in this push"
data-ignore-job-clear-on-click
ng-click="pinAllShownJobs()">
<span class="fa fa-thumb-tack"
data-ignore-job-clear-on-click></span>
</button>
<button class="btn btn-sm btn-resultset trigger-new-jobs-btn"
title="Trigger new jobs"
ng-show="showTriggerButton()"
ng-disabled="!user.loggedin"
data-ignore-job-clear-on-click
ng-click="triggerNewJobs()">
Trigger New Jobs
</button>
<th-action-button></th-action-button>
</span>
</div>
<div class="result-set-body-divider"></div>
<div class="cancel-all-jobs-confirm animate-show" uib-collapse="!showConfirmCancelAll">
<div uib-alert class="alert-danger" close="hideConfirmCancelAll()">
<span class="fa fa-exclamation-triangle"/> This action will cancel all pending and running jobs for this push.
<i>It cannot be undone!</i>
&nbsp;
<button ng-click="cancelAllJobs(resultset.revision)" class="btn btn-xs btn-danger">Confirm</button>
</div>
</div>
<push push="resultset" watch-depth="reference" />
</div>
<!-- Resultset load errors -->
<div ng-show="result_sets.length == 0 && !isLoadingRsBatch.appending &&
locationHasSearchParam('revision') && currentRepo.url"
class="result-set-body unknown-message-body">
<span ng-init="revision=getSearchParamValue('revision')">
<span ng-if="revision !== 'undefined'">
<span>Waiting for a push with revision <strong>{{revision}}</strong></span>
<a href="{{currentRepo.getPushLogHref(revision)}}"
target="_blank"
title="open revision {{revision}} on {{currentRepo.url}}">(view pushlog)</a>
<span class="fa fa-spinner fa-pulse th-spinner"></span>
<div>If the push exists, it will appear in a few minutes once it has been processed.</div>
</span>
<span ng-if="revision ==='undefined'">
<span>This is an invalid revision parameter. Please change it, or click
<a ng-click="changeRepo(repoName)" href="{{::repoName.URL}}">here</a> to reload the latest revisions from {{repoName}}.
</span>
</span>
</span>
</div>
<div ng-show="result_sets.length == 0 && !isLoadingRsBatch.appending &&
!locationHasSearchParam('revision') &&
!locationHasSearchParam('repo') && currentRepo.url"
class="result-set-body unknown-message-body">
<span>
<div><b>No pushes found.</b></div>
<span>No commit information could be loaded for this repository.
More information about this repository can be found <a href="{{::currentRepo.url}}">here</a>.</span>
</span>
</div>
<div ng-show="result_sets.length == 0 && !isLoadingRsBatch.appending &&
locationHasSearchParam('repo') && !currentRepo.url"
class="result-set-body unknown-message-body">
<span>
<div><b>Unknown repository.</b></div>
<span>This repository is either unknown to Treeherder or it doesn't exist.
If this repository does exist, please
<a href="https://bugzilla.mozilla.org/enter_bug.cgi?product=Tree%20Management&component=Treeherder">
file a bug against the Treeherder product in Bugzilla</a> to get it added to the system.
</span>
</span>
</div>
<!-- End resultset clone target -->
<!-- New resultsets progress bar -->
<div class="progress active"
ng-show="isLoadingRsBatch.appending">
<div class="progress-bar progress-bar-striped " role="progressbar" style="width: 100%"></div>
</div>
<!-- Get next resultsets footer -->
<div class="card card-body get-next">
<span>get next:</span>
<div class="btn-group">
<div class="btn btn-light-bordered"
ng-click="getNextResultSets(count, true)"
ng-repeat="count in [10, 20, 50]">{{::count}}</div>
</div>
</div>

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

@ -1,50 +0,0 @@
<span class="btn-group dropdown" uib-dropdown>
<button uib-dropdown-toggle
class="btn btn-sm btn-resultset dropdown-toggle"
type="button"
title="Action menu"
data-hover="dropdown"
data-toggle="dropdown"
data-delay="1000"
data-ignore-job-clear-on-click>
<span class="caret" data-ignore-job-clear-on-click></span>
</button>
<!-- Menu contents -->
<ul class="dropdown-menu pull-right">
<li title="{{user.loggedin ? '' : 'Must be logged in'}}"
class="{{user.loggedin ? '' : 'disabled'}}">
<a target="_blank" data-ignore-job-clear-on-click
title="Add new jobs to this push"
href="" class="dropdown-item"
ng-hide="resultset.isRunnableVisible"
ng-click="showRunnableJobs()">Add new jobs</a></li>
<li><a target="_blank" data-ignore-job-clear-on-click
title="Hide Runnable Jobs"
href="" class="dropdown-item"
ng-show="resultset.isRunnableVisible"
ng-click="deleteRunnableJobs()">Hide Runnable Jobs</a></li>
<li><a target="_blank" data-ignore-job-clear-on-click class="dropdown-item"
href="https://secure.pub.build.mozilla.org/buildapi/self-serve/{{::repoName}}/rev/{{::resultset.revision}}">BuildAPI</a></li>
<li><a target="_blank" data-ignore-job-clear-on-click
href="" class="dropdown-item"
ng-show="user.is_staff && triggerMissingRepos.includes(currentRepo.name)"
ng-click="triggerMissingJobs(resultset.revision)">Trigger missing jobs</a></li>
<li><a target="_blank" data-ignore-job-clear-on-click
href="" class="dropdown-item"
ng-show="user.is_staff"
ng-click="triggerAllTalosJobs(resultset.revision)">Trigger all Talos jobs</a></li>
<li><a target="_blank" data-ignore-job-clear-on-click class="dropdown-item"
href="https://bugherder.mozilla.org/?cset={{::resultset.revision}}&amp;tree={{::repoName}}"
title="Use Bugherder to mark the bugs in this push">Mark with Bugherder</a></li>
<li><a target="_blank" data-ignore-job-clear-on-click
href="" class="dropdown-item"
ng-click="customPushAction()"
title="View/Edit/Submit Action tasks for this push">Custom Push Action...</a></li>
<li><a target="_blank" class="dropdown-item"
href="{{toChangeValue()}}&tochange={{resultset.revision}}" prevent-default-on-left-click
ng-click="jobFilters.addFilter('tochange', resultset.revision)">Set as top of range</a></li>
<li><a target="_blank" class="dropdown-item"
href="{{fromChangeValue()}}&fromchange={{resultset.revision}}" prevent-default-on-left-click
ng-click="jobFilters.addFilter('fromchange', resultset.revision)">Set as bottom of range</a></li>
</ul>
</span>

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

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

@ -1,5 +1,5 @@
<span class="btn-group">
<span class="btn pinned-job {{ ::job.display.btnClass }}"
<span class="btn pinned-job {{ ::job.btnClass }}"
ng-class="{'btn-lg selected-job': (selectedJob==job), 'btn-xs': (selectedJob!=job)}"
title="{{ hoverText }}"
ng-click="viewJob(job)"

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

@ -1,5 +0,0 @@
<span class="result-set-progress">
<span ng-if="percentComplete == 100">- Complete -</span>
<span ng-if="percentComplete < 100"
title="Proportion of jobs that are complete">{{percentComplete}}% - {{inProgress}} in progress</span>
</span>

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

@ -1,11 +1,12 @@
import { Queue, slugid } from 'taskcluster-client-web';
import thTaskcluster from '../js/services/taskcluster';
import { getStatus } from '../helpers/jobHelper';
treeherder.controller('PluginCtrl', [
'$scope', '$rootScope', '$location', '$http', '$interpolate', '$uibModal',
'thUrl', 'ThJobClassificationModel',
'thClassificationTypes', 'ThJobModel', 'thEvents', 'dateFilter', 'thDateFormat',
'numberFilter', 'ThBugJobMapModel', 'thResultStatus', 'thJobFilters',
'numberFilter', 'ThBugJobMapModel', 'thJobFilters',
'ThLog', '$q', 'thPinboard',
'ThJobDetailModel', 'thBuildApi', 'thNotify', 'ThJobLogUrlModel', 'ThModelErrors', 'ThTaskclusterErrors',
'thTabs', '$timeout', 'thReftestStatus', 'ThResultSetStore',
@ -14,7 +15,7 @@ treeherder.controller('PluginCtrl', [
$scope, $rootScope, $location, $http, $interpolate, $uibModal,
thUrl, ThJobClassificationModel,
thClassificationTypes, ThJobModel, thEvents, dateFilter, thDateFormat,
numberFilter, ThBugJobMapModel, thResultStatus, thJobFilters,
numberFilter, ThBugJobMapModel, thJobFilters,
ThLog, $q, thPinboard,
ThJobDetailModel, thBuildApi, thNotify, ThJobLogUrlModel, ThModelErrors, ThTaskclusterErrors, thTabs,
$timeout, thReftestStatus, ThResultSetStore, PhSeries,
@ -92,7 +93,7 @@ treeherder.controller('PluginCtrl', [
successTab = 'perfDetails';
}
if (thResultStatus(job) === 'success') {
if (getStatus(job) === 'success') {
$scope.tabService.selectedTab = successTab;
} else {
$scope.tabService.selectedTab = failTab;
@ -191,7 +192,7 @@ treeherder.controller('PluginCtrl', [
if ($scope.job_log_urls.length) {
$scope.reftestUrl = reftestUrlRoot + $scope.job_log_urls[0].url + "&only_show_unexpected=1";
}
$scope.resultStatusShading = "result-status-shading-" + thResultStatus($scope.job);
$scope.resultStatusShading = "result-status-shading-" + getStatus($scope.job);
var performanceData = _.flatten(Object.values(results[3]));
if (performanceData) {

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

@ -1,12 +1,14 @@
import { getBtnClass, getStatus } from "../../helpers/jobHelper";
treeherder.controller('SimilarJobsPluginCtrl', [
'$scope', 'ThLog', 'ThJobModel', 'ThTextLogStepModel', 'thResultStatusInfo',
'$scope', 'ThLog', 'ThJobModel', 'ThTextLogStepModel',
'numberFilter', 'dateFilter', 'thClassificationTypes',
'thResultStatus', 'ThResultSetModel', 'thNotify',
'ThResultSetModel', 'thNotify',
'thTabs',
function SimilarJobsPluginCtrl(
$scope, ThLog, ThJobModel, ThTextLogStepModel, thResultStatusInfo,
$scope, ThLog, ThJobModel, ThTextLogStepModel,
numberFilter, dateFilter, thClassificationTypes,
thResultStatus, ThResultSetModel, thNotify,
ThResultSetModel, thNotify,
thTabs) {
var $log = new ThLog(this.constructor.name);
@ -85,20 +87,14 @@ treeherder.controller('SimilarJobsPluginCtrl', [
$scope.similar_jobs = [];
$scope.result_status_info = thResultStatusInfo;
$scope.similar_jobs_filters = {
machine_id: false,
build_platform_id: true,
option_collection_hash: true
};
$scope.button_class = function (job) {
var resultState = job.result;
if (job.state !== "completed") {
resultState = job.state;
}
return thResultStatusInfo(resultState).btnClass;
};
$scope.button_class = job => (
getBtnClass(getStatus(job))
);
// this is triggered by the show more link
$scope.show_next = function () {
@ -112,7 +108,7 @@ treeherder.controller('SimilarJobsPluginCtrl', [
ThJobModel.get($scope.repoName, job.id)
.then(function (job) {
$scope.similar_job_selected = job;
$scope.similar_job_selected.result_status = thResultStatus($scope.similar_job_selected);
$scope.similar_job_selected.result_status = getStatus($scope.similar_job_selected);
var duration = (
$scope.similar_job_selected.end_timestamp - $scope.similar_job_selected.start_timestamp
)/60;