зеркало из https://github.com/mozilla/treeherder.git
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:
Родитель
7295cf324a
Коммит
f984acf9a8
|
@ -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');
|
||||
|
|
|
@ -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}&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,
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
||||
<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}}&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;
|
||||
|
|
Загрузка…
Ссылка в новой задаче