Bug 1450022 - Convert the rest of Details Panel to ReactJS (#3621)

This commit is contained in:
Cameron Dawson 2018-06-13 15:40:38 -07:00 коммит произвёл GitHub
Родитель 8114711e2e
Коммит 15721f009c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
80 изменённых файлов: 2602 добавлений и 2766 удалений

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

@ -1 +0,0 @@
ui/vendor/

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

@ -12,4 +12,3 @@ Contents:
:maxdepth: 2
installation
plugin

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

@ -1,96 +0,0 @@
Writing a Plugin
================
When a job is selected, a bottom tabbed panel is activated which shows details
of that job. You can add your own tab to that panel in the form of a
``plugin``.
The existing ``Jobs Detail`` tab is, itself, a plugin. So it is a good example
to follow. See ``ui/plugins/jobdetail``.
To create a new plugin the following steps are required:
* Create your plugin folder
* Create a ``controller`` in your plugin folder
* Create a ``partial`` HTML file in your plugin folder
* Register the ``controller``
* Register the ``partial``
Create your plugin folder
-------------------------
Your folder can have whatever name you choose, but it should reside beneath
``app/plugins``. For example: ``app/plugins/jobfoo``.
Create a controller
-------------------
The most basic of controllers would look like this::
treeherder.controller('JobFooPluginCtrl',
function JobFooPluginCtrl($scope) {
$scope.$watch('selectedJob', function(newValue, oldValue) {
// preferred way to get access to the selected job
if (newValue) {
$scope.job = newValue;
}
}, true);
}
);
This controller just watches the value of ``selectedJob`` to see when it gets
a value. ``selectedJob`` is set by the ui when a job is... well... selected.
Create a partial
----------------
The ``partial`` is the portion of HTML that will be displayed in your plugin's
tab. A very simple partial would look like this::
<div ng-controller="JobFooPluginCtrl">
<p>I pitty the foo that don't like job_guid: {{ job.job_guid }}</p>
</div>
Register the controller
-----------------------
Due to a limitation of jqlite, you must register your ``controller.js`` in
the main application's ``index.html`` file. You can see at the end of the file
that many ``.js`` files are registered. Simply add yours to the list::
<script src="plugins/jobfoo/controller.js"></script>
Register the partial
--------------------
The plugins controller needs to be told to use your plugin. So edit the file:
``app/plugins/controller.js`` and add an entry to the ``tabs`` array with the
information about your plugin::
$scope.tabs = [
{
title: "Jobs Detail",
content: "plugins/jobdetail/main.html",
active: true
},
{
title: "Jobs Foo",
content: "plugins/jobfoo/main.html"
}
];
It may be obvious, but ``title`` is the title of the tab to display. And
``content`` is the path to your partial.
Profit
------
That's it! Reload your page, and you should now have a tab to your plugin!
Rejoice in the profit!

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

@ -65,6 +65,7 @@
"react-router-dom": "4.2.2",
"react-select": "1.2.1",
"react-table": "6.8.6",
"react-tabs": "2.2.2",
"react2angular": "4.0.2",
"reactstrap": "6.0.1",
"redux": "4.0.0",

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

@ -95,8 +95,8 @@ class Treeherder(Base):
self._get_next(50)
@property
def info_panel(self):
return self.InfoPanel(self)
def details_panel(self):
return self.DetailsPanel(self)
def _keyboard_shortcut(self, shortcut):
self.find_element(By.CSS_SELECTOR, 'body').send_keys(shortcut)
@ -123,14 +123,14 @@ class Treeherder(Base):
def select_next_unclassified_job(self):
self._keyboard_shortcut('n')
self.wait.until(lambda _: self.info_panel.is_open)
self.wait.until(lambda _: self.details_panel.is_open)
def select_previous_job(self):
self._keyboard_shortcut(Keys.ARROW_LEFT)
def select_previous_unclassified_job(self):
self._keyboard_shortcut('p')
self.wait.until(lambda _: self.info_panel.is_open)
self.wait.until(lambda _: self.details_panel.is_open)
def select_repository(self, name):
self.find_element(*self._repo_menu_locator).click()
@ -230,8 +230,8 @@ class Treeherder(Base):
_job_groups_locator = (By.CSS_SELECTOR, '.job-group')
_jobs_locator = (By.CSS_SELECTOR, '.job-btn.filter-shown')
_pin_all_jobs_locator = (By.CLASS_NAME, 'pin-all-jobs-btn')
_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)')
_set_bottom_of_range_locator = (By.CLASS_NAME, 'bottom-of-range-menu-item')
_set_top_of_range_locator = (By.CLASS_NAME, 'top-of-range-menu-item')
@property
def author(self):
@ -288,7 +288,7 @@ class Treeherder(Base):
def click(self):
self.root.click()
self.wait.until(lambda _: self.page.info_panel.is_open)
self.wait.until(lambda _: self.page.details_panel.is_open)
@property
def selected(self):
@ -334,10 +334,10 @@ class Treeherder(Base):
def comment(self):
return self.find_element(*self._comment_locator).text
class InfoPanel(Region):
class DetailsPanel(Region):
_root_locator = (By.ID, 'info-panel')
_close_locator = (By.CSS_SELECTOR, '.info-panel-navbar-controls a')
_root_locator = (By.ID, 'details-panel')
_close_locator = (By.CSS_SELECTOR, '.details-panel-close-btn')
_loading_locator = (By.CSS_SELECTOR, '.overlay')
def close(self, method='pointer'):
@ -355,13 +355,13 @@ class Treeherder(Base):
@property
def job_details(self):
return self.JobDetails(self.page)
return self.SummaryPanel(self.page)
class JobDetails(Region):
class SummaryPanel(Region):
_root_locator = (By.ID, 'job-details-panel')
_root_locator = (By.ID, 'summary-panel')
_keywords_locator = (By.CSS_SELECTOR, 'a[title="Filter jobs containing these keywords"]')
_log_viewer_locator = (By.ID, 'logviewer-btn')
_log_viewer_locator = (By.CLASS_NAME, 'logviewer-btn')
_pin_job_locator = (By.ID, 'pin-job-btn')
_result_locator = (By.CSS_SELECTOR, '#result-status-pane div:nth-of-type(1) span')
@ -399,7 +399,7 @@ class Treeherder(Base):
class Pinboard(Region):
_root_locator = (By.ID, 'pinboard-panel')
_clear_all_locator = (By.CSS_SELECTOR, '#pinboard-controls .dropdown-menu li:nth-child(5) a')
_clear_all_locator = (By.CSS_SELECTOR, '#pinboard-controls .dropdown-menu li:nth-child(3) a')
_jobs_locator = (By.CLASS_NAME, 'pinned-job')
_save_menu_locator = (By.CSS_SELECTOR, '#pinboard-controls .save-btn-dropdown')

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

@ -25,4 +25,4 @@ def test_filter_jobs_by_failure_result(base_url, selenium, test_jobs, result):
getattr(filters, 'toggle_{}_jobs'.format(result))()
assert len(page.all_jobs) == 1
page.all_jobs[0].click()
assert page.info_panel.job_details.result == result
assert page.details_panel.job_details.result == result

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

@ -22,8 +22,8 @@ def test_filter_jobs_by_keywords_from_job_panel(base_url, selenium, test_jobs):
page = Treeherder(selenium, base_url).open()
page.wait.until(lambda _: len(page.all_jobs) == len(test_jobs))
page.all_jobs[0].click()
keywords = page.info_panel.job_details.keywords
page.info_panel.job_details.filter_by_keywords()
keywords = page.details_panel.job_details.keywords
page.details_panel.job_details.filter_by_keywords()
page.wait.until(lambda _: len(page.all_jobs) < len(test_jobs))
assert page.quick_filter_term == keywords.lower()

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

@ -37,7 +37,7 @@ def test_filter_failures(base_url, selenium, test_jobs):
page.toggle_in_progress()
page.wait.until(lambda _: len(page.all_jobs) == 1)
page.all_jobs[0].click()
assert page.info_panel.job_details.result == 'testfailed'
assert page.details_panel.job_details.result == 'testfailed'
def test_filter_success(base_url, selenium, test_jobs):
@ -49,7 +49,7 @@ def test_filter_success(base_url, selenium, test_jobs):
page.toggle_in_progress()
page.wait.until(lambda _: len(page.all_jobs) == 1)
page.all_jobs[0].click()
assert page.info_panel.job_details.result == 'success'
assert page.details_panel.job_details.result == 'success'
def test_filter_retry(base_url, selenium, test_jobs):
@ -61,7 +61,7 @@ def test_filter_retry(base_url, selenium, test_jobs):
page.toggle_in_progress()
page.wait.until(lambda _: len(page.all_jobs) == 1)
page.all_jobs[0].click()
assert page.info_panel.job_details.result == 'retry'
assert page.details_panel.job_details.result == 'retry'
def test_filter_usercancel(base_url, selenium, test_jobs):
@ -73,7 +73,7 @@ def test_filter_usercancel(base_url, selenium, test_jobs):
page.toggle_in_progress()
page.wait.until(lambda _: len(page.all_jobs) == 1)
page.all_jobs[0].click()
assert page.info_panel.job_details.result == 'usercancel'
assert page.details_panel.job_details.result == 'usercancel'
def test_filter_superseded(base_url, selenium, test_jobs):
@ -87,7 +87,7 @@ def test_filter_superseded(base_url, selenium, test_jobs):
page.toggle_in_progress()
page.wait.until(lambda _: len(page.all_jobs) == 1)
page.all_jobs[0].click()
assert page.info_panel.job_details.result == 'superseded'
assert page.details_panel.job_details.result == 'superseded'
def test_filter_in_progress(base_url, selenium, test_jobs):
@ -99,4 +99,4 @@ def test_filter_in_progress(base_url, selenium, test_jobs):
page.toggle_usercancel()
page.wait.until(lambda _: len(page.all_jobs) == 1)
page.all_jobs[0].click()
assert page.info_panel.job_details.result == 'unknown'
assert page.details_panel.job_details.result == 'unknown'

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

@ -8,6 +8,6 @@ def test_close_info_panel(base_url, selenium, test_job, method):
page = Treeherder(selenium, base_url).open()
page.wait.until(lambda _: page.all_jobs)
page.all_jobs[0].click()
assert page.info_panel.is_open
page.info_panel.close(method)
assert not page.info_panel.is_open
assert page.details_panel.is_open
page.details_panel.close(method)
assert not page.details_panel.is_open

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

@ -24,5 +24,5 @@ def test_open_log_viewer(base_url, selenium, log):
page = Treeherder(selenium, base_url).open()
page.wait.until(lambda _: page.all_jobs)
page.all_jobs[0].click()
log_viewer = page.info_panel.job_details.open_log_viewer()
log_viewer = page.details_panel.job_details.open_log_viewer()
assert log_viewer.seed_url in selenium.current_url

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

@ -23,7 +23,7 @@ def test_pin_job(base_url, selenium, test_jobs, method):
page.wait.until(lambda _: len(page.all_jobs) == len(test_jobs))
page.all_jobs[0].click()
assert len(page.pinboard.jobs) == 0
page.info_panel.job_details.pin_job(method)
page.details_panel.job_details.pin_job(method)
assert len(page.pinboard.jobs) == 1
assert page.pinboard.jobs[0].symbol == page.all_jobs[0].symbol
@ -32,7 +32,7 @@ def test_clear_pinboard(base_url, selenium, test_jobs):
page = Treeherder(selenium, base_url).open()
page.wait.until(lambda _: len(page.all_jobs) == len(test_jobs))
page.all_jobs[0].click()
page.info_panel.job_details.pin_job()
page.details_panel.job_details.pin_job()
assert len(page.pinboard.jobs) == 1
page.pinboard.clear()
assert len(page.pinboard.jobs) == 0

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

@ -1,39 +0,0 @@
/* jasmine specs for controllers go here */
describe('PinboardCtrl', function(){
var $httpBackend, controller, pinboardScope;
beforeEach(angular.mock.module('treeherder.app'));
beforeEach(inject(function ($injector, $rootScope, $controller) {
$httpBackend = $injector.get('$httpBackend');
jasmine.getJSONFixtures().fixturesPath='base/tests/ui/mock';
pinboardScope = $rootScope.$new();
$controller('PinboardCtrl', {
'$scope': pinboardScope,
});
}));
/*
Tests PinboardCtrl
*/
it('should determine sha or commit url', function() {
// Blatantly not a sha or commit
var str = "banana";
expect(pinboardScope.isSHAorCommit(str)).toBe(false);
// This contains a legit 12-char SHA but includes a space
str = "c00b13480420 8c2652ebd4f45a1d37277c54e60b";
expect(pinboardScope.isSHAorCommit(str)).toBe(false);
// This is a valid commit URL
str = "https://hg.mozilla.org/integration/mozilla-inbound/rev/c00b134804208c2652ebd4f45a1d37277c54e60b";
expect(pinboardScope.isSHAorCommit(str)).toBe(true);
// Valid 40-char SHA
str = "c00b134804208c2652ebd4f45a1d37277c54e60b";
expect(pinboardScope.isSHAorCommit(str)).toBe(true);
// Valid 12-char SHA
str = "c00b13480420";
expect(pinboardScope.isSHAorCommit(str)).toBe(true);
});
});

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

@ -28,8 +28,6 @@ const serviceContext = require.context('../../../ui/js/services', true, /^\.\/.*
serviceContext.keys().forEach(serviceContext);
const componentContext = require.context('../../../ui/js/components', true, /^\.\/.*\.jsx?$/);
componentContext.keys().forEach(componentContext);
const pluginContext = require.context('../../../ui/plugins', true, /^\.\/.*\.jsx?$/);
pluginContext.keys().forEach(pluginContext);
const testContext = require.context('./', true, /^\.\/.*\.tests\.jsx?$/);
testContext.keys().forEach(testContext);

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

@ -30,9 +30,9 @@ describe('JobGroup component', () => {
);
expect(jobGroup.html()).toEqual(
'<span class="platform-group"><span class="disabled job-group" title="Web platform tests with e10s">' +
'<button class="btn group-symbol" data-ignore-job-clear-on-click="true">W-e10s</button>' +
'<button class="btn group-symbol">W-e10s</button>' +
'<span class="group-content">' +
'<span class="group-job-list"><button data-job-id="166315800" data-ignore-job-clear-on-click="true" title="test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button></span>' +
'<span class="group-job-list"><button data-job-id="166315800" title="test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button></span>' +
'<span class="group-count-list"><button class="btn-dkgray-count btn group-btn btn-xs job-group-count filter-shown" title="2 running jobs in group">2</button>' +
'</span></span></span></span>'
);
@ -50,9 +50,9 @@ describe('JobGroup component', () => {
jobGroup.setState({ expanded: false });
expect(jobGroup.html()).toEqual(
'<span class="platform-group"><span class="disabled job-group" title="Web platform tests with e10s">' +
'<button class="btn group-symbol" data-ignore-job-clear-on-click="true">W-e10s</button>' +
'<button class="btn group-symbol">W-e10s</button>' +
'<span class="group-content">' +
'<span class="group-job-list"><button data-job-id="166315800" data-ignore-job-clear-on-click="true" title="test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button></span>' +
'<span class="group-job-list"><button data-job-id="166315800" title="test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button></span>' +
'<span class="group-count-list"><button class="btn-dkgray-count btn group-btn btn-xs job-group-count filter-shown" title="2 running jobs in group">2</button>' +
'</span></span></span></span>'
);
@ -69,12 +69,12 @@ describe('JobGroup component', () => {
jobGroup.setState({ expanded: true });
expect(jobGroup.html()).toEqual(
'<span class="platform-group"><span class="disabled job-group" title="Web platform tests with e10s">' +
'<button class="btn group-symbol" data-ignore-job-clear-on-click="true">W-e10s</button>' +
'<button class="btn group-symbol">W-e10s</button>' +
'<span class="group-content">' +
'<span class="group-job-list">' +
'<button data-job-id="166315799" data-ignore-job-clear-on-click="true" title="test-linux64/debug-web-platform-tests-wdspec-e10s - " class="btn btn-dkgray filter-shown job-btn btn-xs">Wd</button>' +
'<button data-job-id="166315800" data-ignore-job-clear-on-click="true" title="test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button>' +
'<button data-job-id="166315797" data-ignore-job-clear-on-click="true" title="test-linux64/debug-web-platform-tests-e10s-1 - " class="btn btn-dkgray filter-shown job-btn btn-xs">wpt1</button>' +
'<button data-job-id="166315799" title="test-linux64/debug-web-platform-tests-wdspec-e10s - " class="btn btn-dkgray filter-shown job-btn btn-xs">Wd</button>' +
'<button data-job-id="166315800" title="test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button>' +
'<button data-job-id="166315797" title="test-linux64/debug-web-platform-tests-e10s-1 - " class="btn btn-dkgray filter-shown job-btn btn-xs">wpt1</button>' +
'</span>' +
'<span class="group-count-list"></span></span></span></span>'
);
@ -92,12 +92,12 @@ describe('JobGroup component', () => {
$rootScope.$emit(thEvents.groupStateChanged, 'expanded');
expect(jobGroup.html()).toEqual(
'<span class="platform-group"><span class="disabled job-group" title="Web platform tests with e10s">' +
'<button class="btn group-symbol" data-ignore-job-clear-on-click="true">W-e10s</button>' +
'<button class="btn group-symbol">W-e10s</button>' +
'<span class="group-content">' +
'<span class="group-job-list">' +
'<button data-job-id="166315799" data-ignore-job-clear-on-click="true" title="test-linux64/debug-web-platform-tests-wdspec-e10s - " class="btn btn-dkgray filter-shown job-btn btn-xs">Wd</button>' +
'<button data-job-id="166315800" data-ignore-job-clear-on-click="true" title="test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button>' +
'<button data-job-id="166315797" data-ignore-job-clear-on-click="true" title="test-linux64/debug-web-platform-tests-e10s-1 - " class="btn btn-dkgray filter-shown job-btn btn-xs">wpt1</button>' +
'<button data-job-id="166315799" title="test-linux64/debug-web-platform-tests-wdspec-e10s - " class="btn btn-dkgray filter-shown job-btn btn-xs">Wd</button>' +
'<button data-job-id="166315800" title="test-linux64/debug-web-platform-tests-reftests-e10s-1 - (18 mins)" class="btn btn-green filter-shown job-btn btn-xs">Wr1</button>' +
'<button data-job-id="166315797" title="test-linux64/debug-web-platform-tests-e10s-1 - " class="btn btn-dkgray filter-shown job-btn btn-xs">wpt1</button>' +
'</span>' +
'<span class="group-count-list"></span></span></span></span>'
);
@ -114,9 +114,9 @@ describe('JobGroup component', () => {
expect(jobGroup.html()).toEqual(
'<span class="platform-group"><span class="disabled job-group" title="Spidermonkey builds">' +
'<button class="btn group-symbol" data-ignore-job-clear-on-click="true">SM</button>' +
'<button class="btn group-symbol">SM</button>' +
'<span class="group-content"><span class="group-job-list">' +
'<button data-job-id="166316707" data-ignore-job-clear-on-click="true" title="spidermonkey-sm-msan-linux64/opt - (0 mins)" class="btn btn-dkblue filter-shown job-btn btn-xs">msan</button>' +
'<button data-job-id="166316707" title="spidermonkey-sm-msan-linux64/opt - (0 mins)" class="btn btn-dkblue filter-shown job-btn btn-xs">msan</button>' +
'</span>' +
'<span class="group-count-list">' +
'<button class="btn-green-count btn group-btn btn-xs job-group-count filter-shown" title="6 success jobs in group">6</button>' +
@ -136,10 +136,10 @@ describe('JobGroup component', () => {
jobGroup.setState({ showDuplicateJobs: true });
expect(jobGroup.html()).toEqual(
'<span class="platform-group"><span class="disabled job-group" title="Spidermonkey builds">' +
'<button class="btn group-symbol" data-ignore-job-clear-on-click="true">SM</button>' +
'<button class="btn group-symbol">SM</button>' +
'<span class="group-content"><span class="group-job-list">' +
'<button data-job-id="166316707" data-ignore-job-clear-on-click="true" title="spidermonkey-sm-msan-linux64/opt - (0 mins)" class="btn btn-dkblue filter-shown job-btn btn-xs">msan</button>' +
'<button data-job-id="166321182" data-ignore-job-clear-on-click="true" title="spidermonkey-sm-msan-linux64/opt - (10 mins)" class="btn btn-green filter-shown job-btn btn-xs">msan</button>' +
'<button data-job-id="166316707" title="spidermonkey-sm-msan-linux64/opt - (0 mins)" class="btn btn-dkblue filter-shown job-btn btn-xs">msan</button>' +
'<button data-job-id="166321182" title="spidermonkey-sm-msan-linux64/opt - (10 mins)" class="btn btn-green filter-shown job-btn btn-xs">msan</button>' +
'</span>' +
'<span class="group-count-list">' +
'<button class="btn-green-count btn group-btn btn-xs job-group-count filter-shown" title="5 success jobs in group">5</button>' +
@ -159,10 +159,10 @@ describe('JobGroup component', () => {
$rootScope.$emit(thEvents.duplicateJobsVisibilityChanged);
expect(jobGroup.html()).toEqual(
'<span class="platform-group"><span class="disabled job-group" title="Spidermonkey builds">' +
'<button class="btn group-symbol" data-ignore-job-clear-on-click="true">SM</button>' +
'<button class="btn group-symbol">SM</button>' +
'<span class="group-content"><span class="group-job-list">' +
'<button data-job-id="166316707" data-ignore-job-clear-on-click="true" title="spidermonkey-sm-msan-linux64/opt - (0 mins)" class="btn btn-dkblue filter-shown job-btn btn-xs">msan</button>' +
'<button data-job-id="166321182" data-ignore-job-clear-on-click="true" title="spidermonkey-sm-msan-linux64/opt - (10 mins)" class="btn btn-green filter-shown job-btn btn-xs">msan</button>' +
'<button data-job-id="166316707" title="spidermonkey-sm-msan-linux64/opt - (0 mins)" class="btn btn-dkblue filter-shown job-btn btn-xs">msan</button>' +
'<button data-job-id="166321182" title="spidermonkey-sm-msan-linux64/opt - (10 mins)" class="btn btn-green filter-shown job-btn btn-xs">msan</button>' +
'</span>' +
'<span class="group-count-list">' +
'<button class="btn-green-count btn group-btn btn-xs job-group-count filter-shown" title="5 success jobs in group">5</button>' +

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

@ -2,39 +2,39 @@ strong {
font-weight: bold;
}
div#info-panel {
background-color: #AAAAAA;
details-panel {
font-size: 12px;
height: 35%;
max-height: 75%;
flex: none;
display: flex;
flex-flow: column;
}
.info-panel-slide {
animation: info-panel-slide 0.4s;
.details-panel-slide {
animation: details-panel-slide 0.4s;
height: 100%;
}
@keyframes info-panel-slide {
@keyframes details-panel-slide {
0% { transform: translateY(100%); }
100% { transform: translateY(0%); }
}
div#info-panel .navbar {
div#details-panel .navbar,
div#tabs-panel .tab-headers {
border-radius: 0;
border-style: solid;
border-color: #42484F;
border-width: 1px 0;
height: 33px;
width: 100%;
margin: 0;
font-size: 12px;
min-height: 33px;
min-width: initial;
z-index: 100;
background-color: #252C33;
border: 1px solid transparent;
color: #CED3D9;
justify-content: space-between;
}
div#info-panel-resizer {
div#details-panel-resizer {
display: flex;
flex: none;
background-color: #919dad;
@ -42,49 +42,53 @@ div#info-panel-resizer {
height: 2px;
}
div#info-panel .navbar-nav > ul {
#tab-header-buttons > span > span {
padding-left: 5px;
}
div#details-panel .navbar-nav > ul {
height: 32px;
}
div#info-panel .navbar-nav.actionbar-nav > li > a,
div#info-panel .navbar-nav.actionbar-nav > li > button {
div#details-panel .navbar-nav.actionbar-nav > li > a,
div#details-panel .navbar-nav.actionbar-nav > li > button {
padding: 8px 15px;
line-height: 16px;
}
div#info-panel .navbar-nav.tab-headers > li > a,
div#info-panel .navbar-nav.tab-headers > li > button {
div#details-panel .navbar-nav.tab-headers > li > a,
div#details-panel .navbar-nav.tab-headers > li > button {
padding: 8px 15px;
line-height: 30px;
}
div#info-panel .navbar-nav > li > button {
div#details-panel .navbar-nav > li > button {
border: none;
background: transparent;
}
/* Use a loaded image, rather than an icon, so it needs to be slightly shorter */
div#info-panel .navbar-nav > li > a#logviewer-btn {
div#details-panel .navbar-nav > li > a#logviewer-btn {
line-height: 18px;
}
div#info-panel .navbar-nav > li > a.disabled,
div#info-panel .navbar-nav > li > button.disabled,
div#details-panel .navbar-nav > li > a.disabled,
div#details-panel .navbar-nav > li > button.disabled,
ul.actionbar-menu > li.disabled {
cursor: not-allowed;
text-decoration: none;
}
div#info-panel .navbar-nav > li.active a,
div#info-panel .navbar-nav > li.active a:hover,
div#info-panel .navbar-nav > li.active a:focus {
div#details-panel .navbar-nav > li.active a,
div#details-panel .navbar-nav > li.active a:hover,
div#details-panel .navbar-nav > li.active a:focus {
outline: 0;
}
div#info-panel .info-panel-navbar > ul.tab-headers > li {
div#details-panel .details-panel-navbar > ul.tab-headers > li {
border-right: 1px solid #42484F;
}
.info-panel-navbar {
.details-panel-navbar {
background-color: #252C33;
border: 1px solid transparent;
color: #CED3D9;
@ -94,18 +98,34 @@ div#info-panel .info-panel-navbar > ul.tab-headers > li {
height: 33px;
}
.info-panel-navbar li {
.details-panel-navbar li {
align-self: center;
}
.info-panel-navbar-tabs {
justify-content: space-between;
.tab-header-tabs {
flex-direction: row;
display: flex;
}
.tab-headers {
.tab-header-tabs > li {
padding: 1px 15px;
line-height: 30px;
cursor: pointer;
color: #9FA3A5;
}
#details-panel ul.tab-headers {
list-style: none;
flex-direction: row;
min-width: 550px;
display: flex;
padding-left: 0;
font-size: 12px;
}
.details-panel-close-btn {
padding-top: 3px;
font-size: 12px;
}
.perf-job-selected {
@ -113,80 +133,107 @@ div#info-panel .info-panel-navbar > ul.tab-headers > li {
min-width: 646px !important;
}
.info-panel-navbar-controls {
.details-panel-navbar-controls {
flex-wrap: nowrap;
}
.info-panel-navbar .navbar-nav {
.details-panel-navbar .navbar-nav {
display: flex;
flex-direction: row;
}
.info-panel-navbar .navbar-nav > li {
.details-panel-navbar .navbar-nav > li {
white-space: nowrap;
}
.info-panel-navbar .navbar-nav > li > a,
.info-panel-navbar .navbar-nav > li > button {
.details-panel-navbar .navbar-nav > li > a,
.details-panel-navbar .navbar-nav > li > .btn {
color: #9FA3A5;
padding: 7px 15px;
padding: 4px 15px;
}
div#info-panel .navbar-nav > li > a:hover,
div#info-panel .navbar-nav > li > a:focus,
div#info-panel .navbar-nav > li > button:hover,
div#info-panel .navbar-nav > li > button:focus
div#details-panel .navbar-nav > li > a:hover,
div#details-panel .navbar-nav > li > a:focus,
div#details-panel .navbar-nav > li > button:hover,
div#details-panel .navbar-nav > li > button:focus
{
background-color: #1E252B;
color: #D3D8DA;
}
div#info-panel .navbar-nav > li > a:active,
div#info-panel .navbar-nav > li > button:active
div#details-panel .navbar-nav > li > a:active,
div#details-panel .navbar-nav > li > button:active
{
background-color: #000;
}
div#info-panel .navbar-nav > li > a.disabled:active,
div#info-panel .navbar-nav > li > button.disabled:active
div#details-panel .navbar-nav > li > a.disabled:active,
div#details-panel .navbar-nav > li > button.disabled:active
{
background-color: #1E252B;
}
.info-panel-navbar .actionbar-nav {
.details-panel-navbar .actionbar-nav {
flex: auto;
}
div#info-panel .info-panel-navbar .navbar-nav > li.active a,
div#info-panel .info-panel-navbar .navbar-nav > li.active a:hover,
div#info-panel .info-panel-navbar .navbar-nav > li.active a:focus,
div#info-panel .info-panel-navbar > li.active a,
div#info-panel .info-panel-navbar > li.active a:hover,
div#info-panel .info-panel-navbar > li.active a:focus {
div#details-panel .details-panel-navbar .navbar-nav > li.active a,
div#details-panel .details-panel-navbar .navbar-nav > li.active a:hover,
div#details-panel .details-panel-navbar .navbar-nav > li.active a:focus,
div#details-panel .details-panel-navbar > li.active a,
div#details-panel .details-panel-navbar > li.active a:hover,
div#details-panel .details-panel-navbar > li.active a:focus {
background-color: #1A4666;
color: #EEF0F2;
}
#info-panel-content {
.tab-header-tabs > li.selected-tab {
background-color: #1A4666;
color: #EEF0F2;
}
.react-tabs {
height: 100%;
width: 100%;
}
.react-tabs__tab-panel--selected {
height: 100%;
}
#tabs-panel {
height: 100%;
max-height: calc(100% - 35px);
width: 100%;
}
#details-panel #job-details-list,
#details-panel .failure-summary-list,
#details-panel .similar-jobs > .similar-job-list tbody {
overflow-y: auto;
height: 100%;
}
#details-panel {
position: relative; /* So we can absolutely position the loading overlay */
height: 60%;
height: 100%;
flex: auto;
display: flex;
flex-flow: row;
}
#job-details-panel, #job-tabs-panel {
#summary-panel {
background-color: #fff;
display: flex;
flex-flow: column;
}
#job-details-actionbar, #job-tabs-navbar {
#job-details-actionbar {
min-height: 33px;
}
/*
* Job details action bar
* action bar
*/
.action-bar-spin {
@ -202,6 +249,10 @@ div#info-panel .info-panel-navbar > li.active a:focus {
flex-direction: row;
}
.actionbar-nav .btn {
cursor: pointer;
}
.actionbar-nav > li {
/* Override padding on all icons to keep compact */
padding: 0 !important;
@ -245,24 +296,29 @@ div#info-panel .info-panel-navbar > li.active a:focus {
* Job details panel (left side)
*/
#job-details-panel {
width: 260px;
min-width: 260px;
#summary-panel-content {
overflow-y: auto;
}
#job-details-panel .content-spacer {
#summary-panel {
width: 260px;
min-width: 260px;
height: 100%;
}
#summary-panel .content-spacer {
padding-top: 2px;
padding-bottom: 4px;
}
#job-details-panel ul li {
#summary-panel ul li {
padding: 0 0 0 5px;
line-height: 15px;
word-wrap: break-word;
}
#job-details-panel ul li label {
padding: 0;
#summary-panel ul li label {
padding: 0 3px 0 0;
margin: 2px 0;
font-weight: bold;
}
@ -312,7 +368,7 @@ div#info-panel .info-panel-navbar > li.active a:focus {
color: grey;
}
#job-details-panel em.testfail {
#summary-panel em.testfail {
color: red;
}
@ -326,6 +382,11 @@ div#info-panel .info-panel-navbar > li.active a:focus {
overflow: auto;
}
#result-status-pane {
width: 100%;
padding: 4px;
}
#result-status-pane div {
display: inline-block;
}
@ -342,50 +403,24 @@ div#info-panel .info-panel-navbar > li.active a:focus {
border-left: 1px solid lightgrey;
}
.job-tabs-content {
padding: 2px 4px 0;
}
#job-tabs-panel {
flex: 1 6;
padding: 0;
min-width: 565px;
}
#job-tabs-pane {
max-height: calc(100% - 33px);
flex: 1;
display: flex;
overflow: auto;
}
#job-tabs-pane > * {
flex: 1;
display: flex;
}
#job-tabs-pane > * > * {
flex: 1;
display: flex;
}
#job-tabs-pane > * > * > * {
flex: 1;
display: flex;
#job-details-list label {
margin-left: 2px;
}
/*
* Failure summary
*/
#job-tabs-panel ul.failure-summary-list {
ul.failure-summary-list {
width: 100%;
margin-bottom: 0;
height: 100%;
}
ul.failure-summary-list li {
font-size: 11px;
background: #ccfaff;
padding: 1px 0 0 2px;
}
ul.failure-summary-list li .btn-xs {
@ -486,48 +521,49 @@ annotations-tab {
font-size: 12px;
}
.similar_jobs {
.similar-jobs {
display: flex;
flex-flow: row;
height: 100%;
}
div.similar_jobs .right_panel {
div.similar-jobs .similar-job-detail-panel {
border-left: 1px solid #101010;
margin-right: 1px;
overflow-y: auto;
flex: 1 1;
}
div.similar_jobs .right_panel form {
div.similar-jobs .similar-job-detail-panel form {
overflow: hidden;
background-color: #D3D3D3;
}
div.similar_jobs .right_panel form .checkbox input[type="checkbox"] {
div.similar-jobs .similar-job-detail-panel form .checkbox input[type="checkbox"] {
margin-left: 0;
position: relative;
}
div.similar_jobs .right_panel .similar_job_detail {
div.similar-jobs .similar-job-detail-panel .similar_job_detail {
border-top: 1px solid #101010;
}
div.similar_jobs .right_panel .similar_job_detail table {
div.similar-jobs .similar-job-detail-panel .similar_job_detail table {
width: 100%;
overflow: hidden;
}
div.similar_jobs .left_panel {
div.similar-jobs .similar-job-list {
overflow: auto;
flex: 1 1;
}
div.similar_jobs .left_panel table {
div.similar-jobs .similar-job-list table {
margin-bottom: 7px;
}
/* We override bootstrap table style for cleaner layout */
div.similar_jobs .left_panel table tr > td {
div.similar-jobs .similar-job-list table tr > td {
vertical-align: middle;
border-top: 1px solid lightgrey;
border-bottom: 0;
@ -536,14 +572,14 @@ div.similar_jobs .left_panel table tr > td {
}
/* Selected Similar Job row in blue */
div.similar_jobs .left_panel table tr.active > td {
div.similar-jobs .similar-job-list table tr.active > td {
background: #e2ebfa;
border-top: 1px solid darkgrey;
border-bottom: 1px solid darkgrey;
}
/* Avoid using the hand pointer unless we are on a link */
div.similar_jobs .left_panel table tr {
div.similar-jobs .similar-job-list table tr {
cursor: default;
}

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

@ -13,7 +13,7 @@ html, body {
height: 100%;
font-size: 14px;
line-height: 1.42;
overflow-x: hidden;
overflow: hidden;
}
body {

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

@ -1,13 +1,15 @@
#job-tabs-navbar .info-panel-navbar #pinboard-btn {
margin-top: -2px;
#pinboard-btn {
margin-top: 0;
margin-bottom: 1px;
background-color: #e6eef5;
color: #252c33;
line-height: 30px;
line-height: 22px;
cursor: pointer;
border-radius: 0;
padding: 3px 10px 4px 14px;
}
.pinboard-btn-text {
margin: 0 15px;
font-size: 12px;
}
@ -50,12 +52,13 @@
line-height: 18px;
}
#pinboard-panel {
#pinboard-contents {
background-color: #e6eef5;
color: #252c33;
flex: auto;
display: flex;
flex-flow: row;
min-height: 100px;
}
#pinboard-panel .header {
@ -132,6 +135,15 @@
.add-related-bugs-input {
width: 12em;
margin: -3px 0 0 -3px;
font-size: 12px;
line-height: 12px;
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.pinboard-related-bug-preload-txt {
@ -165,8 +177,17 @@
color: black;
}
#pinboard-classification select {
#pinboard-classification-content .form-group {
margin-bottom: 5px;
margin-top: 5px;
}
select#pinboard-classification-select.classification-select,
select#pinboard-revision-select.classification-select {
width: 177px;
height: 20px;
font-size: 12px;
padding: 0;
}
.add-classification-input {

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

@ -13,7 +13,7 @@ import './css/treeherder-global.css';
import './css/treeherder-navbar.css';
import './css/treeherder-navbar-panels.css';
import './css/treeherder-notifications.css';
import './css/treeherder-info-panel.css';
import './css/treeherder-details-panel.css';
import './css/treeherder-job-buttons.css';
import './css/treeherder-resultsets.css';
import './css/treeherder-pinboard.css';
@ -25,12 +25,12 @@ import './js/treeherder_app';
// Treeherder React UI
import './job-view/PushList';
import './job-view/details/DetailsPanel';
// Treeherder JS
import './js/components/auth';
import './js/directives/treeherder/main';
import './js/directives/treeherder/top_nav_bar';
import './js/directives/treeherder/bottom_nav_panel';
import './js/services/main';
import './js/services/buildapi';
import './js/services/taskcluster';
@ -48,14 +48,4 @@ import './js/controllers/notification';
import './js/controllers/filters';
import './js/controllers/bugfiler';
import './js/controllers/tcjobactions';
import './plugins/tabs';
import './plugins/controller';
import './plugins/pinboard';
import './job-view/details/summary/SummaryPanel';
import './job-view/details/tabs/JobDetailsTab';
import './job-view/details/tabs/failureSummary/FailureSummaryTab';
import './job-view/details/tabs/autoclassify/AutoclassifyTab';
import './job-view/details/tabs/AnnotationsTab';
import './job-view/details/tabs/SimilarJobsTab';
import './job-view/details/tabs/PerformanceTab';
import './js/filters';

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

@ -2,6 +2,8 @@ import $ from 'jquery';
import _ from 'lodash';
import { thPlatformMap } from '../js/constants';
import { toDateStr } from './display';
import { getSlaveHealthUrl, getWorkerExplorerUrl } from './url';
const btnClasses = {
busted: "btn-red",
@ -17,6 +19,14 @@ const btnClasses = {
'in progress': "btn-dkgray",
};
// 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;
};
// 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.
@ -35,12 +45,8 @@ export const getBtnClass = function getBtnClass(resultState, failureClassificati
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;
export const getJobBtnClass = function getJobBtnClass(job) {
return getBtnClass(getStatus(job), job.failure_classification_id);
};
export const isReftest = function isReftest(job) {
@ -75,7 +81,7 @@ const isOnScreen = function isOnScreen(el) {
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;
viewport.bottom = $(window).height() - $('#details-panel').height() - 20;
const bounds = {};
bounds.top = el.offset().top;
bounds.bottom = bounds.top + el.outerHeight();
@ -129,5 +135,46 @@ export const getSearchStr = function getSearchStr(job) {
job.job_type_name,
`${symbolInfo}(${job.job_type_symbol})`
].filter(item => typeof item !== 'undefined').join(' ');
};
export const getHoverText = function getHoverText(job) {
const duration = Math.round((job.end_timestamp - job.start_timestamp) / 60);
return `${job.job_type_name} - ${getStatus(job)} - ${duration} mins`;
};
export const getJobMachineUrl = function getJobMachineUrl(job) {
const { build_system_type, machine_name } = job;
const machineUrl = (machine_name !== 'unknown' && build_system_type === 'buildbot') ?
getSlaveHealthUrl(machine_name) :
getWorkerExplorerUrl(job.taskcluster_metadata.task_id);
return machineUrl;
};
export const getTimeFields = function getTimeFields(job) {
// time fields to show in detail panel, but that should be grouped together
const timeFields = {
requestTime: toDateStr(job.submit_timestamp)
};
// If start time is 0, then duration should be from requesttime to now
// If we have starttime and no endtime, then duration should be starttime to now
// If we have both starttime and endtime, then duration will be between those two
const endtime = job.end_timestamp || Date.now() / 1000;
const starttime = job.start_timestamp || job.submit_timestamp;
const duration = `${Math.round((endtime - starttime)/60, 0)} minute(s)`;
if (job.start_timestamp) {
timeFields.startTime = toDateStr(job.start_timestamp);
timeFields.duration = duration;
} else {
timeFields.duration = `Not started (queued for ${duration})`;
}
if (job.end_timestamp) {
timeFields.endTime = toDateStr(job.end_timestamp);
}
return timeFields;
};

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

@ -21,3 +21,7 @@ export const isSHA = function isSHA(str) {
}
return true;
};
export const isSHAorCommit = function isSHAorCommit(str) {
return /^[a-f\d]{12,40}$/.test(str) || str.includes('hg.mozilla.org');
};

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

@ -35,11 +35,12 @@
/>
</span>
</div>
<div ng-controller="PluginCtrl"
id="info-panel"
ng-show="selectedJob"
ng-include src="'plugins/pluginpanel.html'">
</div>
<details-panel
repo-name="repoName"
selected-job="selectedJob"
user="user"
ng-show="selectedJob"
/>
</div>
<th-notification-box></th-notification-box>

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

@ -101,7 +101,6 @@ export default class JobButtonComponent extends React.Component {
const classes = ['btn', btnClass, 'filter-shown'];
const attributes = {
'data-job-id': id,
'data-ignore-job-clear-on-click': true,
title
};

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

@ -16,7 +16,6 @@ class GroupSymbol extends React.PureComponent {
return (
<button
className="btn group-symbol"
data-ignore-job-clear-on-click
onClick={toggleExpanded}
>{groupSymbol}{tier !== 1 && <span className="small text-muted">[tier {tier}]</span>}
</button>

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

@ -116,7 +116,7 @@ export default class Push extends React.Component {
}
render() {
const { push, loggedIn, isStaff, $injector, repoName } = this.props;
const { push, isLoggedIn, isStaff, $injector, repoName } = this.props;
const { watched, runnableVisible } = this.state;
const { currentRepo, urlBasePath } = this.$rootScope;
const { id, push_timestamp, revision, job_counts, author } = push;
@ -130,7 +130,7 @@ export default class Push extends React.Component {
revision={revision}
jobCounts={job_counts}
watchState={watched}
loggedIn={loggedIn}
isLoggedIn={isLoggedIn}
isStaff={isStaff}
repoName={repoName}
urlBasePath={urlBasePath}
@ -165,13 +165,13 @@ export default class Push extends React.Component {
Push.propTypes = {
push: PropTypes.object.isRequired,
$injector: PropTypes.object.isRequired,
loggedIn: PropTypes.bool,
isLoggedIn: PropTypes.bool,
isStaff: PropTypes.bool,
repoName: PropTypes.string,
};
Push.defaultProps = {
loggedIn: false,
isLoggedIn: false,
isStaff: false,
repoName: 'mozilla-inbound',
};

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

@ -106,7 +106,7 @@ export default class PushActionMenu extends React.PureComponent {
}
render() {
const { loggedIn, isStaff, repoName, revision, pushId, runnableVisible,
const { isLoggedIn, isStaff, repoName, revision, pushId, runnableVisible,
hideRunnableJobsCb, showRunnableJobsCb } = this.props;
const { topOfRangeUrl, bottomOfRangeUrl } = this.state;
@ -132,8 +132,8 @@ export default class PushActionMenu extends React.PureComponent {
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'}
title={isLoggedIn ? 'Add new jobs to this push' : 'Must be logged in'}
className={isLoggedIn ? 'dropdown-item' : 'dropdown-item disabled'}
onClick={() => showRunnableJobsCb()}
>Add new jobs</li>
}
@ -168,11 +168,11 @@ export default class PushActionMenu extends React.PureComponent {
title="View/Edit/Submit Action tasks for this push"
>Custom Push Action...</li>
<li><a
className="dropdown-item"
className="dropdown-item top-of-range-menu-item"
href={topOfRangeUrl}
>Set as top of range</a></li>
<li><a
className="dropdown-item"
className="dropdown-item bottom-of-range-menu-item"
href={bottomOfRangeUrl}
>Set as bottom of range</a></li>
</ul>
@ -184,7 +184,7 @@ export default class PushActionMenu extends React.PureComponent {
PushActionMenu.propTypes = {
runnableVisible: PropTypes.bool.isRequired,
isStaff: PropTypes.bool.isRequired,
loggedIn: PropTypes.bool.isRequired,
isLoggedIn: PropTypes.bool.isRequired,
revision: PropTypes.string.isRequired,
repoName: PropTypes.string.isRequired,
pushId: PropTypes.number.isRequired,

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

@ -4,7 +4,7 @@ import { Alert } from 'reactstrap';
import PushActionMenu from './PushActionMenu';
import { toDateStr } from '../helpers/display';
import { formatModelError, formatTaskclusterError } from '../helpers/errorMessage';
import { thPinboardCountError, thEvents } from "../js/constants";
import { thEvents } from '../js/constants';
function Author(props) {
const authorMatch = props.author.match(/\<(.*?)\>+/);
@ -12,7 +12,7 @@ function Author(props) {
return (
<span title="View pushes by this user" className="push-author">
<a href={props.url} data-ignore-job-clear-on-click>{authorEmail}</a>
<a href={props.url}>{authorEmail}</a>
</span>
);
}
@ -58,7 +58,6 @@ export default class PushHeader extends React.PureComponent {
this.$rootScope = $injector.get('$rootScope');
this.thJobFilters = $injector.get('thJobFilters');
this.thNotify = $injector.get('thNotify');
this.thPinboard = $injector.get('thPinboard');
this.thBuildApi = $injector.get('thBuildApi');
this.ThResultSetStore = $injector.get('ThResultSetStore');
this.ThResultSetModel = $injector.get('ThResultSetModel');
@ -112,13 +111,13 @@ export default class PushHeader extends React.PureComponent {
}
triggerNewJobs() {
const { loggedIn, pushId } = this.props;
const { isLoggedIn, pushId } = this.props;
if (!window.confirm(
'This will trigger all selected jobs. Click "OK" if you want to proceed.')) {
return;
}
if (loggedIn) {
if (isLoggedIn) {
const builderNames = this.ThResultSetStore.getSelectedRunnableJobs(pushId);
this.ThResultSetStore.getGeckoDecisionTaskId(pushId).then((decisionTaskID) => {
this.ThResultSetModel.triggerNewJobs(builderNames, decisionTaskID).then((result) => {
@ -136,10 +135,10 @@ export default class PushHeader extends React.PureComponent {
}
cancelAllJobs() {
const { repoName, revision, loggedIn, pushId } = this.props;
const { repoName, revision, isLoggedIn, pushId } = this.props;
this.setState({ showConfirmCancelAll: false });
if (!loggedIn) return;
if (!isLoggedIn) return;
this.ThResultSetModel.cancelAll(pushId).then(() => (
this.thBuildApi.cancelAll(repoName, revision)
@ -153,16 +152,8 @@ export default class PushHeader extends React.PureComponent {
}
pinAllShownJobs() {
if (!this.thPinboard.spaceRemaining()) {
this.thNotify.send(thPinboardCountError, 'danger');
return;
}
const shownJobs = this.ThResultSetStore.getAllShownJobs(
this.thPinboard.spaceRemaining(),
thPinboardCountError,
this.props.pushId
);
this.thPinboard.pinJobs(shownJobs);
const shownJobs = this.ThResultSetStore.getAllShownJobs(this.props.pushId);
this.$rootScope.$emit(thEvents.pinJobs, shownJobs);
if (!this.$rootScope.selectedJob) {
this.$rootScope.$emit(thEvents.jobClick, shownJobs[0]);
@ -170,11 +161,11 @@ export default class PushHeader extends React.PureComponent {
}
render() {
const { repoName, loggedIn, pushId, isStaff, jobCounts, author,
const { repoName, isLoggedIn, pushId, isStaff, jobCounts, author,
revision, runnableVisible, $injector, watchState,
showRunnableJobsCb, hideRunnableJobsCb, cycleWatchState } = this.props;
const { filterParams } = this.state;
const cancelJobsTitle = loggedIn ?
const cancelJobsTitle = isLoggedIn ?
"Cancel all jobs" :
"Must be logged in to cancel jobs";
const counts = jobCounts || { pending: 0, running: 0, completed: 0 };
@ -194,7 +185,6 @@ export default class PushHeader extends React.PureComponent {
<a
href={`${this.revisionPushFilterUrl}${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} />
@ -221,40 +211,35 @@ export default class PushHeader extends React.PureComponent {
rel="noopener"
title="View details on failed test results for this push"
>View Tests</a>
{loggedIn &&
{isLoggedIn &&
<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}
isLoggedIn={isLoggedIn}
isStaff={isStaff || false}
runnableVisible={runnableVisible}
revision={revision}
@ -301,7 +286,7 @@ PushHeader.propTypes = {
cycleWatchState: PropTypes.func.isRequired,
jobCounts: PropTypes.object,
watchState: PropTypes.string,
loggedIn: PropTypes.bool,
isLoggedIn: PropTypes.bool,
isStaff: PropTypes.bool,
urlBasePath: PropTypes.string,
};
@ -309,7 +294,7 @@ PushHeader.propTypes = {
PushHeader.defaultProps = {
jobCounts: null,
watchState: 'none',
loggedIn: false,
isLoggedIn: false,
isStaff: false,
urlBasePath: '',
};

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

@ -25,7 +25,6 @@ export default class PushList extends React.Component {
this.$location = $injector.get('$location');
this.$timeout = $injector.get('$timeout');
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');
@ -34,7 +33,7 @@ export default class PushList extends React.Component {
this.getNextPushes = this.getNextPushes.bind(this);
this.updateUrlFromchange = this.updateUrlFromchange.bind(this);
this.clearJobOnClick = this.clearJobOnClick.bind(this);
this.closeJob = this.closeJob.bind(this);
this.state = {
pushList: [],
@ -80,8 +79,8 @@ export default class PushList extends React.Component {
}
});
this.clearSelectedJobUnlisten = this.$rootScope.$on(thEvents.clearSelectedJob, () => {
this.$location.search('selectedJob', null);
this.clearSelectedJobUnlisten = this.$rootScope.$on(thEvents.clearSelectedJob, (ev, target) => {
this.closeJob(target);
});
this.changeSelectionUnlisten = this.$rootScope.$on(
@ -188,7 +187,7 @@ export default class PushList extends React.Component {
// 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
// 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
@ -232,14 +231,14 @@ export default class PushList extends React.Component {
// 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();
this.$rootScope.$emit(thEvents.clearSelectedJob);
}
}
noMoreUnclassifiedFailures() {
this.$timeout(() => {
this.thNotify.send("No unclassified failures to select.");
this.$rootScope.closeJob();
this.$rootScope.$emit(thEvents.clearSelectedJob);
});
}
@ -254,33 +253,39 @@ export default class PushList extends React.Component {
}, 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()) {
// Clear the selectedJob
closeJob() {
// TODO: Should block clearing the selected job if there are pinned jobs
// But can't get the pinned jobs at this time. When we're completely on React,
// or at least have a shared parent between PushList and DetailsPanel, we can share
// a PinBoardModel or Context so they both have access.
if (!this.$rootScope.countPinnedJobs()) {
const selected = findSelectedInstance();
if (selected) {
selected.setSelected(false);
}
this.$timeout(this.$rootScope.closeJob);
}
}
clearIfEligibleTarget(target) {
if (target.hasAttribute("data-job-clear-on-click")) {
this.$rootScope.$emit(thEvents.clearSelectedJob, target);
}
}
render() {
const { $injector, user, repoName, revision, currentRepo } = this.props;
const { pushList, loadingPushes, jobsReady } = this.state;
const { loggedin, is_staff } = user;
const { isLoggedIn, isStaff } = user;
return (
<div onClick={this.clearJobOnClick}>
<div onClick={evt => this.clearIfEligibleTarget(evt.target)}>
{jobsReady && <span className="hidden ready" />}
{repoName && pushList.map(push => (
<Push
push={push}
loggedIn={loggedin || false}
isStaff={is_staff}
isLoggedIn={isLoggedIn || false}
isStaff={isStaff}
repoName={repoName}
$injector={$injector}
key={push.id}
@ -300,7 +305,7 @@ export default class PushList extends React.Component {
revision={revision}
/>
}
<div className="card card-body get-next">
<div className="card card-body get-next" data-job-clear-on-click>
<span>get next:</span>
<div className="btn-group">
{[10, 20, 50].map(count => (

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

@ -55,7 +55,6 @@ export class Revision extends React.PureComponent {
<a
title={`Open revision ${commitRevision} on ${repo.url}`}
href={repo.getRevisionHref(commitRevision)}
data-ignore-job-clear-on-click
>{commitRevision.substring(0, 12)}
</a>
</span>

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

@ -14,7 +14,7 @@ export class RevisionList extends React.PureComponent {
const { push, repo } = this.props;
return (
<span className="revision-list col-5">
<span className="revision-list col-5" data-job-clear-on-click>
<ul className="list-unstyled">
{push.revisions.map(revision =>
(<Revision
@ -47,7 +47,6 @@ export function MoreRevisionsLink(props) {
<li>
<a
href={props.href}
data-ignore-job-clear-on-click
target="_blank"
rel="noopener"
>{`\u2026and more`}<i className="fa fa-external-link-square" /></a>

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

@ -0,0 +1,514 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import { chunk } from 'lodash';
import $ from 'jquery';
import treeherder from '../../js/treeherder';
import {
thEvents,
thBugSuggestionLimit,
thPinboardCountError,
thPinboardMaxSize,
} from '../../js/constants';
import { getLogViewerUrl, getReftestUrl } from '../../helpers/url';
import BugJobMapModel from '../../models/bugJobMap';
import BugSuggestionsModel from '../../models/bugSuggestions';
import JobClassificationModel from '../../models/classification';
import JobModel from '../../models/job';
import JobDetailModel from '../../models/jobDetail';
import JobLogUrlModel from '../../models/jobLogUrl';
import TextLogStepModel from '../../models/textLogStep';
import PinBoard from './PinBoard';
import SummaryPanel from './summary/SummaryPanel';
import TabsPanel from './tabs/TabsPanel';
class DetailsPanel extends React.Component {
constructor(props) {
super(props);
const { $injector } = this.props;
this.PhSeries = $injector.get('PhSeries');
this.ThResultSetStore = $injector.get('ThResultSetStore');
this.thClassificationTypes = $injector.get('thClassificationTypes');
this.thNotify = $injector.get('thNotify');
this.$rootScope = $injector.get('$rootScope');
this.$location = $injector.get('$location');
this.$timeout = $injector.get('$timeout');
// used to cancel all the ajax requests triggered by selectJob
this.selectJobController = null;
this.state = {
isPinBoardVisible: false,
jobDetails: [],
jobLogUrls: [],
jobDetailLoading: false,
jobLogsAllParsed: false,
logViewerUrl: null,
logViewerFullUrl: null,
reftestUrl: null,
perfJobDetail: [],
jobRevision: null,
logParseStatus: 'unavailable',
classifications: [],
bugs: [],
suggestions: [],
errors: [],
bugSuggestionsLoading: false,
pinnedJobs: {},
pinnedJobBugs: {},
};
}
componentDidMount() {
this.pinJob = this.pinJob.bind(this);
this.unPinJob = this.unPinJob.bind(this);
this.unPinAll = this.unPinAll.bind(this);
this.addBug = this.addBug.bind(this);
this.removeBug = this.removeBug.bind(this);
this.closeJob = this.closeJob.bind(this);
this.countPinnedJobs = this.countPinnedJobs.bind(this);
// give access to this count to components that don't have a common ancestor in React
// TODO: remove this once we're fully on ReactJS: Bug 1450042
this.$rootScope.countPinnedJobs = this.countPinnedJobs;
this.jobClickUnlisten = this.$rootScope.$on(thEvents.jobClick, (evt, job) => {
this.setState({
jobDetailLoading: true,
jobDetails: [],
suggestions: [],
isPinBoardVisible: !!this.countPinnedJobs(),
}, () => this.selectJob(job));
});
this.clearSelectedJobUnlisten = this.$rootScope.$on(thEvents.clearSelectedJob, () => {
if (this.selectJobController !== null) {
this.selectJobController.abort();
}
if (!this.countPinnedJobs()) {
this.closeJob();
}
});
this.toggleJobPinUnlisten = this.$rootScope.$on(thEvents.toggleJobPin, (event, job) => {
this.toggleJobPin(job);
});
this.jobPinUnlisten = this.$rootScope.$on(thEvents.jobPin, (event, job) => {
this.pinJob(job);
});
this.jobsClassifiedUnlisten = this.$rootScope.$on(thEvents.jobsClassified, () => {
this.updateClassifications(this.props.selectedJob);
});
this.pinAllShownJobsUnlisten = this.$rootScope.$on(thEvents.pinJobs, (event, jobs) => {
this.pinJobs(jobs);
});
this.clearPinboardUnlisten = this.$rootScope.$on(thEvents.clearPinboard, () => {
if (this.state.isPinBoardVisible) {
this.unPinAll();
}
});
this.pulsePinCountUnlisten = this.$rootScope.$on(thEvents.pulsePinCount, () => {
this.pulsePinCount();
});
}
componentWillUnmount() {
this.jobClickUnlisten();
this.clearSelectedJobUnlisten();
this.toggleJobPinUnlisten();
this.jobPinUnlisten();
this.jobsClassifiedUnlisten();
this.clearPinboardUnlisten();
this.pulsePinCountUnlisten();
this.pinAllShownJobsUnlisten();
}
getRevisionTips() {
return this.ThResultSetStore.getPushArray().map(push => ({
revision: push.revision,
author: push.author,
title: push.revisions[0].comments.split('\n')[0]
}));
}
togglePinBoardVisibility() {
this.setState({ isPinBoardVisible: !this.state.isPinBoardVisible });
}
loadBugSuggestions(job) {
const { repoName } = this.props;
BugSuggestionsModel.get(job.id).then((suggestions) => {
suggestions.forEach((suggestion) => {
suggestion.bugs.too_many_open_recent = (
suggestion.bugs.open_recent.length > thBugSuggestionLimit
);
suggestion.bugs.too_many_all_others = (
suggestion.bugs.all_others.length > thBugSuggestionLimit
);
suggestion.valid_open_recent = (
suggestion.bugs.open_recent.length > 0 &&
!suggestion.bugs.too_many_open_recent
);
suggestion.valid_all_others = (
suggestion.bugs.all_others.length > 0 &&
!suggestion.bugs.too_many_all_others &&
// If we have too many open_recent bugs, we're unlikely to have
// relevant all_others bugs, so don't show them either.
!suggestion.bugs.too_many_open_recent
);
});
// if we have no bug suggestions, populate with the raw errors from
// the log (we can do this asynchronously, it should normally be
// fast)
if (!suggestions.length) {
TextLogStepModel.get(job.id).then((textLogSteps) => {
const errors = textLogSteps
.filter(step => step.result !== 'success')
.map(step => ({
name: step.name,
result: step.result,
logViewerUrl: getLogViewerUrl(job.id, repoName, step.finished_line_number)
}));
this.setState({ errors });
});
}
this.setState({ bugSuggestionsLoading: false, suggestions });
});
}
async updateClassifications(job) {
const classifications = await JobClassificationModel.getList({ job_id: job.id });
const bugs = await BugJobMapModel.getList({ job_id: job.id });
this.setState({ classifications, bugs });
}
selectJob(newJob) {
const { repoName } = this.props;
if (this.selectJobController !== null) {
// Cancel the in-progress fetch requests.
this.selectJobController.abort();
}
// eslint-disable-next-line no-undef
this.selectJobController = new AbortController();
let jobDetails = [];
const jobPromise = JobModel.get(
repoName, newJob.id,
this.selectJobController.signal);
const jobDetailPromise = JobDetailModel.getJobDetails(
{ job_guid: newJob.job_guid },
this.selectJobController.signal);
const jobLogUrlPromise = JobLogUrlModel.getList(
{ job_id: newJob.id },
this.selectJobController.signal);
const phSeriesPromise = this.PhSeries.getSeriesData(
repoName, { job_id: newJob.id });
Promise.all([
jobPromise,
jobDetailPromise,
jobLogUrlPromise,
phSeriesPromise
]).then(async (results) => {
// The first result comes from the job promise.
// This version of the job has more information than what we get in the main job list. This
// is what we'll pass to the rest of the details panel. It has extra fields like
// taskcluster_metadata.
const job = results[0];
const jobRevision = this.ThResultSetStore.getPush(job.result_set_id).revision;
// the second result comes from the job detail promise
jobDetails = results[1];
// incorporate the buildername into the job details if this is a buildbot job
// (i.e. it has a buildbot request id)
const buildbotRequestIdDetail = jobDetails.find(detail => detail.title === 'buildbot_request_id');
if (buildbotRequestIdDetail) {
jobDetails = [...jobDetails, { title: 'Buildername', value: job.ref_data_name }];
}
// the third result comes from the jobLogUrl promise
// exclude the json log URLs
const jobLogUrls = results[2].filter(log => !log.name.endsWith('_json'));
let logParseStatus = 'unavailable';
// Provide a parse status as a scope variable for logviewer shortcut
if (jobLogUrls.length && jobLogUrls[0].parse_status) {
logParseStatus = jobLogUrls[0].parse_status;
}
// Provide a parse status for the model
const jobLogsAllParsed = (jobLogUrls ?
jobLogUrls.every(jlu => jlu.parse_status !== 'pending') :
false);
const logViewerUrl = getLogViewerUrl(job.id, repoName);
const logViewerFullUrl = `${location.origin}/${logViewerUrl}`;
const reftestUrl = jobLogUrls.length ?
`${getReftestUrl(jobLogUrls[0].url)}&only_show_unexpected=1` :
'';
const performanceData = Object.values(results[3]).reduce((a, b) => [...a, ...b], []);
let perfJobDetail = [];
if (performanceData) {
const signatureIds = [...new Set(performanceData.map(perf => perf.signature_id))];
const seriesListList = await Promise.all(chunk(signatureIds, 20).map(
signatureIdChunk => this.PhSeries.getSeriesList(repoName, { id: signatureIdChunk })
));
const seriesList = seriesListList.reduce((a, b) => [...a, ...b], []);
perfJobDetail = performanceData.map(d => ({
series: seriesList.find(s => d.signature_id === s.id),
...d
})).filter(d => !d.series.parentSignature).map(d => ({
url: `/perf.html#/graphs?series=${[repoName, d.signature_id, 1, d.series.frameworkId]}&selected=${[repoName, d.signature_id, job.result_set_id, d.id]}`,
value: d.value,
title: d.series.name
}));
}
this.setState({
job,
jobLogUrls,
jobDetails,
jobLogsAllParsed,
logParseStatus,
logViewerUrl,
logViewerFullUrl,
reftestUrl,
perfJobDetail,
jobRevision,
}, async () => {
await this.updateClassifications(job);
await this.loadBugSuggestions(job);
this.setState({ jobDetailLoading: false });
});
}).finally(() => {
this.selectJobController = null;
});
}
closeJob() {
this.$rootScope.selectedJob = null;
this.ThResultSetStore.setSelectedJob();
this.$location.search('selectedJob', null);
if (this.selectJobController) {
this.selectJobController.abort();
}
this.setState({ isPinboardVisible: false });
}
toggleJobPin(job) {
const { pinnedJobs } = this.state;
if (pinnedJobs.includes(job)) {
this.unPinJob(job);
} else {
this.pinJob(job);
}
if (!this.selectedJob) {
this.selectJob(job);
}
}
pulsePinCount() {
$('.pin-count-group').addClass('pin-count-pulse');
window.setTimeout(() => {
$('.pin-count-group').removeClass('pin-count-pulse');
}, 700);
}
pinJob(job) {
const { pinnedJobs } = this.state;
if (thPinboardMaxSize - this.countPinnedJobs() > 0) {
this.setState({
pinnedJobs: { ...pinnedJobs, [job.id]: job },
isPinBoardVisible: true,
});
this.pulsePinCount();
} else {
this.thNotify.send(thPinboardCountError, 'danger');
}
if (!this.state.selectedJob) {
this.selectJob(job);
}
}
unPinJob(id) {
const { pinnedJobs } = this.state;
delete pinnedJobs[id];
this.setState({ pinnedJobs: { ...pinnedJobs } });
}
pinJobs(jobsToPin) {
const { pinnedJobs } = this.state;
const spaceRemaining = thPinboardMaxSize - this.countPinnedJobs();
const showError = jobsToPin.length > spaceRemaining;
const newPinnedJobs = jobsToPin.slice(0, spaceRemaining).reduce((acc, job) => ({ ...acc, [job.id]: job }), {});
if (!spaceRemaining) {
this.thNotify.send(thPinboardCountError, 'danger', { sticky: true });
this.$rootScope.$apply();
return;
}
this.setState({
pinnedJobs: { ...pinnedJobs, ...newPinnedJobs },
isPinBoardVisible: true,
}, () => {
if (!this.props.selectedJob) {
this.$rootScope.$emit(thEvents.jobClick, jobsToPin[0]);
}
if (showError) {
this.thNotify.send(thPinboardCountError, 'danger', { sticky: true });
this.$rootScope.$apply();
}
});
}
countPinnedJobs() {
return Object.keys(this.state.pinnedJobs).length;
}
addBug(bug, job) {
const { pinnedJobBugs } = this.state;
pinnedJobBugs[bug.id] = bug;
this.setState({ pinnedJobBugs: { ...pinnedJobBugs } });
if (job) {
this.pinJob(job);
}
}
removeBug(id) {
const { pinnedJobBugs } = this.state;
delete pinnedJobBugs[id];
this.setState({ pinnedJobBugs: { ...pinnedJobBugs } });
}
unPinAll() {
this.setState({
pinnedJobs: {},
pinnedJobBugs: {},
});
}
render() {
const {
repoName, $injector, user, currentRepo,
} = this.props;
const {
job, isPinBoardVisible, jobDetails, jobRevision, jobLogUrls, jobDetailLoading,
perfJobDetail, suggestions, errors, bugSuggestionsLoading, logParseStatus,
classifications, logViewerUrl, logViewerFullUrl, pinnedJobs, pinnedJobBugs, bugs, reftestUrl,
} = this.state;
return (
<div className={job ? 'details-panel-slide' : 'hidden'}>
<div
id="details-panel-resizer"
resizer="horizontal"
resizer-height="6"
resizer-bottom="#details-panel"
/>
<PinBoard
isVisible={isPinBoardVisible}
selectedJob={job}
isLoggedIn={user.isLoggedIn || false}
classificationTypes={this.thClassificationTypes}
revisionList={this.getRevisionTips()}
pinnedJobs={pinnedJobs}
pinnedJobBugs={pinnedJobBugs}
addBug={this.addBug}
removeBug={this.removeBug}
pinJob={this.pinJob}
unPinJob={this.unPinJob}
unPinAll={this.unPinAll}
$injector={$injector}
/>
{!!job && <div id="details-panel">
<SummaryPanel
repoName={repoName}
selectedJob={job}
jobLogUrls={jobLogUrls}
logParseStatus={logParseStatus}
jobDetailLoading={jobDetailLoading}
latestClassification={classifications.length ? classifications[0] : null}
isTryRepo={currentRepo.isTryRepo}
logViewerUrl={logViewerUrl}
logViewerFullUrl={logViewerFullUrl}
pinJob={this.pinJob}
bugs={bugs}
user={user}
$injector={$injector}
/>
<span className="job-tabs-divider" />
<TabsPanel
jobDetails={jobDetails}
perfJobDetail={perfJobDetail}
selectedJob={job}
repoName={repoName}
jobRevision={jobRevision}
suggestions={suggestions}
errors={errors}
bugSuggestionsLoading={bugSuggestionsLoading}
logParseStatus={logParseStatus}
classifications={classifications}
classificationTypes={this.thClassificationTypes}
jobLogUrls={jobLogUrls}
isPinBoardVisible={isPinBoardVisible}
pinnedJobs={pinnedJobs}
bugs={bugs}
addBug={this.addBug}
pinJob={this.pinJob}
togglePinBoardVisibility={() => this.togglePinBoardVisibility()}
logViewerFullUrl={logViewerFullUrl}
reftestUrl={reftestUrl}
user={user}
$injector={$injector}
/>
</div>}
<div id="clipboard-container"><textarea id="clipboard" /></div>
</div>
);
}
}
DetailsPanel.propTypes = {
$injector: PropTypes.object.isRequired,
repoName: PropTypes.string.isRequired,
selectedJob: PropTypes.object,
user: PropTypes.object,
currentRepo: PropTypes.object,
};
DetailsPanel.defaultProps = {
selectedJob: null,
user: { isLoggedIn: false, isStaff: false, email: null },
currentRepo: { isTryRepo: true },
};
treeherder.component('detailsPanel', react2angular(
DetailsPanel,
['repoName', 'selectedJob', 'user'],
['$injector']));

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

@ -0,0 +1,548 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Form, FormGroup, Input, FormFeedback } from 'reactstrap';
import $ from 'jquery';
import Mousetrap from 'mousetrap';
import { thEvents } from '../../js/constants';
import { formatModelError } from '../../helpers/errorMessage';
import { getJobBtnClass, getHoverText } from '../../helpers/job';
import { isSHAorCommit } from '../../helpers/revision';
import { getBugUrl } from '../../helpers/url';
import BugJobMapModel from '../../models/bugJobMap';
import JobClassificationModel from '../../models/classification';
import JobModel from '../../models/job';
export default class PinBoard extends React.Component {
constructor(props) {
super(props);
const { $injector } = this.props;
this.thNotify = $injector.get('thNotify');
this.$timeout = $injector.get('$timeout');
this.ThResultSetStore = $injector.get('ThResultSetStore');
this.$rootScope = $injector.get('$rootScope');
this.state = {
failureClassificationId: 4,
failureClassificationComment: '',
enteringBugNumber: false,
newBugNumber: null,
classification: {},
};
}
componentDidMount() {
this.bugNumberKeyPress = this.bugNumberKeyPress.bind(this);
this.save = this.save.bind(this);
this.handleRelatedBugDocumentClick = this.handleRelatedBugDocumentClick.bind(this);
this.unPinAll = this.unPinAll.bind(this);
this.retriggerAllPinnedJobs = this.retriggerAllPinnedJobs.bind(this);
this.addRelatedBugUnlisten = this.$rootScope.$on(thEvents.addRelatedBug, (event, job) => {
this.props.pinJob(job);
this.toggleEnterBugNumber(true);
});
this.saveClassificationUnlisten = this.$rootScope.$on(thEvents.saveClassification, () => {
this.save();
});
}
componentWillUnmount() {
this.addRelatedBugUnlisten();
this.saveClassificationUnlisten();
}
setClassificationId(evt) {
this.setState({ failureClassificationId: parseInt(evt.target.value) });
}
setClassificationText(evt) {
this.setState({ failureClassificationComment: evt.target.value });
}
unPinAll() {
this.props.unPinAll();
this.setState({
classification: this.createNewClassification()
});
}
save() {
const { isLoggedIn, pinnedJobs } = this.props;
let errorFree = true;
if (this.state.enteringBugNumber) {
// we should save this for the user, as they likely
// just forgot to hit enter. Returns false if invalid
errorFree = this.saveEnteredBugNumber();
if (!errorFree) {
this.$timeout(this.thNotify.send('Please enter a valid bug number', 'danger'));
}
}
if (!this.canSaveClassifications() && isLoggedIn) {
this.$timeout(this.thNotify.send('Please classify this failure before saving', 'danger'));
errorFree = false;
}
if (!isLoggedIn) {
this.$timeout(this.thNotify.send('Must be logged in to save job classifications', 'danger'));
errorFree = false;
}
if (errorFree) {
const jobs = Object.values(pinnedJobs);
const classifyPromises = jobs.map(job => this.saveClassification(job));
const bugPromises = jobs.map(job => this.saveBugs(job));
Promise.all([...classifyPromises, ...bugPromises]).then(() => {
this.$rootScope.$emit(thEvents.jobsClassified, { jobs: [...jobs] });
this.unPinAll();
this.completeClassification();
this.setState({ classification: this.createNewClassification() });
});
// HACK: it looks like Firefox on Linux and Windows doesn't
// want to accept keyboard input after this change for some
// reason which I don't understand. Chrome (any platform)
// or Firefox on Mac works fine though.
document.activeElement.blur();
}
}
createNewClassification() {
const { email } = this.props;
const { failureClassificationId, failureClassificationComment } = this.state;
return new JobClassificationModel({
text: failureClassificationComment,
who: email,
failure_classification_id: failureClassificationId
});
}
saveClassification(job) {
const classification = this.createNewClassification();
// classification can be left unset making this a no-op
if (classification.failure_classification_id > 0) {
job.failure_classification_id = classification.failure_classification_id;
// update the unclassified failure count for the page
this.ThResultSetStore.updateUnclassifiedFailureMap(job);
classification.job_id = job.id;
return classification.create().then(() => {
this.thNotify.send(`Classification saved for ${job.platform} ${job.job_type_name}`, 'success');
}).catch((response) => {
const message = `Error saving classification for ${job.platform} ${job.job_type_name}`;
this.thNotify.send(formatModelError(response, message), 'danger');
});
}
}
saveBugs(job) {
const { pinnedJobBugs } = this.props;
Object.values(pinnedJobBugs).forEach((bug) => {
const bjm = new BugJobMapModel({
bug_id: bug.id,
job_id: job.id,
type: 'annotation'
});
bjm.create()
.then(() => {
this.thNotify.send(`Bug association saved for ${job.platform} ${job.job_type_name}`, 'success');
})
.catch((response) => {
const message = `Error saving bug association for ${job.platform} ${job.job_type_name}`;
this.thNotify.send(formatModelError(response, message), 'danger');
});
});
}
// If the pasted data is (or looks like) a 12 or 40 char SHA,
// or if the pasted data is an hg.m.o url, automatically select
// the 'fixed by commit' classification type
pasteSHA(evt) {
const pastedData = evt.originalEvent.clipboardData.getData('text');
if (isSHAorCommit(pastedData)) {
this.state.classification.failure_classification_id = 2;
}
}
retriggerAllPinnedJobs() {
// pushing pinned jobs to a list.
const jobIds = Object.keys(this.props.pinnedJobs);
JobModel.retrigger(this.$rootScope.repoName, jobIds);
}
cancelAllPinnedJobsTitle() {
if (!this.props.isLoggedIn) {
return 'Not logged in';
} else if (!this.canCancelAllPinnedJobs()) {
return 'No pending / running jobs in pinBoard';
}
return 'Cancel all the pinned jobs';
}
canCancelAllPinnedJobs() {
const cancellableJobs = Object.values(this.props.pinnedJobs).filter(
job => (job.state === 'pending' || job.state === 'running'));
return this.props.isLoggedIn && cancellableJobs.length > 0;
}
async cancelAllPinnedJobs() {
if (window.confirm('This will cancel all the selected jobs. Are you sure?')) {
await JobModel.cancel(this.$rootScope.repoName, Object.keys(this.props.pinnedJobs));
this.unPinAll();
}
}
canSaveClassifications() {
const thisClass = this.state.classification;
const { pinnedJobBugs, isLoggedIn } = this.props;
return this.hasPinnedJobs() && isLoggedIn &&
(!!Object.keys(pinnedJobBugs).length ||
(thisClass.failure_classification_id !== 4 && thisClass.failure_classification_id !== 2) ||
this.$rootScope.currentRepo.is_try_repo ||
this.$rootScope.currentRepo.repository_group.name === 'project repositories' ||
(thisClass.failure_classification_id === 4 && thisClass.text.length > 0) ||
(thisClass.failure_classification_id === 2 && thisClass.text.length > 7));
}
// Facilitates Clear all if no jobs pinned to reset pinBoard UI
pinboardIsDirty() {
return this.state.classification.text !== '' ||
!!Object.keys(this.props.pinnedJobBugs).length ||
this.state.classification.failure_classification_id !== 4;
}
// Dynamic btn/anchor title for classification save
saveUITitle(category) {
let title = '';
if (!this.props.isLoggedIn) {
title = title.concat('not logged in / ');
}
if (category === 'classification') {
if (!this.canSaveClassifications()) {
title = title.concat('ineligible classification data / ');
}
if (!this.hasPinnedJobs()) {
title = title.concat('no pinned jobs');
}
// We don't check pinned jobs because the menu dropdown handles it
} else if (category === 'bug') {
if (!this.hasPinnedJobBugs()) {
title = title.concat('no related bugs');
}
}
if (title === '') {
title = `Save ${category} data`;
} else {
// Cut off trailing '/ ' if one exists, capitalize first letter
title = title.replace(/\/ $/, '');
title = title.replace(/^./, l => l.toUpperCase());
}
return title;
}
hasPinnedJobs() {
return !!Object.keys(this.props.pinnedJobs).length;
}
hasPinnedJobBugs() {
return !!Object.keys(this.props.pinnedJobBugs).length;
}
handleRelatedBugDocumentClick(event) {
if (!$(event.target).hasClass('add-related-bugs-input')) {
this.saveEnteredBugNumber();
$(document).off('click', this.handleRelatedBugDocumentClick);
}
}
toggleEnterBugNumber(tf) {
this.setState({
enteringBugNumber: tf,
}, () => {
$('#related-bug-input').focus();
});
// document.off('click', this.handleRelatedBugDocumentClick);
if (tf) {
// Rebind escape to canceling the bug entry, pressing escape
// again will close the pinBoard as usual.
Mousetrap.bind('escape', () => {
const cancel = this.toggleEnterBugNumber.bind(this, false);
cancel();
});
// Install a click handler on the document so that clicking
// outside of the input field will close it. A blur handler
// can't be used because it would have timing issues with the
// click handler on the + icon.
window.setTimeout(() => {
$(document).on('click', this.handleRelatedBugDocumentClick);
}, 0);
}
}
completeClassification() {
this.$rootScope.$broadcast('blur-this', 'classification-comment');
}
isNumber(text) {
return !text || /^[0-9]*$/.test(text);
}
saveEnteredBugNumber() {
const { newBugNumber } = this.state;
if (this.state.enteringBugNumber) {
if (!newBugNumber) {
this.toggleEnterBugNumber(false);
} else if (this.isNumber(newBugNumber)) {
this.props.addBug({ id: parseInt(newBugNumber) });
this.toggleEnterBugNumber(false);
return true;
}
}
}
bugNumberKeyPress(ev) {
if (ev.key === 'Enter') {
this.saveEnteredBugNumber(ev.target.value);
if (ev.ctrlKey) {
// If ctrl+enter, then save the classification
this.save();
}
ev.preventDefault();
}
}
viewJob(job) {
this.$rootScope.selectedJob = job;
this.$rootScope.$emit(thEvents.jobClick, job);
}
render() {
const {
selectedJob, revisionList, isLoggedIn, isVisible, classificationTypes,
pinnedJobs, pinnedJobBugs, removeBug, unPinJob,
} = this.props;
const {
failureClassificationId, failureClassificationComment,
enteringBugNumber, newBugNumber,
} = this.state;
const selectedJobId = selectedJob ? selectedJob.id : null;
return (
<div
id="pinboard-panel"
className={isVisible ? '' : 'hidden'}
>
<div id="pinboard-contents">
<div id="pinned-job-list">
<div className="content">
{!this.hasPinnedJobs() && <span
className="pinboard-preload-txt"
>press spacebar to pin a selected job</span>}
{Object.values(pinnedJobs).map(job => (
<span className="btn-group" key={job.id}>
<span
className={`btn pinned-job ${getJobBtnClass(job)} ${selectedJobId === job.id ? 'btn-lg selected-job' : 'btn-xs'}`}
title={getHoverText(job)}
onClick={() => this.viewJob(job)}
data-job-id={job.job_id}
>{job.job_type_symbol}</span>
<span
className={`btn btn-ltgray pinned-job-close-btn ${selectedJobId === job.id ? 'btn-lg selected-job' : 'btn-xs'}`}
onClick={() => unPinJob(job.id)}
title="un-pin this job"
><i className="fa fa-times" /></span>
</span>
))}
</div>
</div>
{/* Related bugs */}
<div id="pinboard-related-bugs">
<div className="content">
<span
onClick={() => this.toggleEnterBugNumber(!enteringBugNumber)}
className="pointable"
title="Add a related bug"
><i className="fa fa-plus-square add-related-bugs-icon" /></span>
{!this.hasPinnedJobBugs() && <span
className="pinboard-preload-txt pinboard-related-bug-preload-txt"
onClick={() => {
this.toggleEnterBugNumber(!enteringBugNumber);
}}
>click to add a related bug</span>}
{enteringBugNumber && <span
className="add-related-bugs-form"
>
<Input
id="related-bug-input"
data-bug-input
type="text"
pattern="[0-9]*"
className="add-related-bugs-input"
placeholder="enter bug number"
invalid={!this.isNumber(newBugNumber)}
onKeyPress={this.bugNumberKeyPress}
onChange={ev => this.setState({ newBugNumber: ev.target.value })}
/>
<FormFeedback>Please enter only numbers</FormFeedback>
</span>}
{Object.values(pinnedJobBugs).map(bug => (<span key={bug.id}>
<span className="btn-group pinboard-related-bugs-btn">
<a
className="btn btn-xs related-bugs-link"
title={bug.summary}
href={getBugUrl(bug.id)}
target="_blank"
rel="noopener"
><em>{bug.id}</em></a>
<span
className="btn btn-ltgray btn-xs pinned-job-close-btn"
onClick={() => removeBug(bug.id)}
title="remove this bug"
><i className="fa fa-times" /></span>
</span>
</span>))}
</div>
</div>
{/* Classification dropdown */}
<div id="pinboard-classification">
<div className="pinboard-label">classification</div>
<div id="pinboard-classification-content" className="content">
<Form onSubmit={this.completeClassification} className="form">
<FormGroup>
<Input
type="select"
name="failureClassificationId"
id="pinboard-classification-select"
className="classification-select"
onChange={evt => this.setClassificationId(evt)}
>
{classificationTypes.classificationOptions.map(opt => (
<option value={opt.id} key={opt.id}>{opt.name}</option>
))}
</Input>
</FormGroup>
{/* Classification comment */}
<div className="classification-comment-container">
<input
id="classification-comment"
type="text"
className="form-control add-classification-input"
onChange={evt => this.setClassificationText(evt)}
onPaste={this.pasteSHA}
placeholder="click to add comment"
value={failureClassificationComment}
/>
{/*blur-this*/}
{failureClassificationId === 2 && <div>
<FormGroup>
<Input
id="pinboard-revision-select"
className="classification-select"
type="select"
defaultValue={0}
onChange={evt => this.setClassificationText(evt)}
>
<option value="0" disabled>Choose a recent
commit
</option>
{revisionList.slice(0, 20).map(tip => (<option
title={tip.title}
value={tip.revision}
key={tip.revision}
>{tip.revision.slice(0, 12)} {tip.author}</option>))}
</Input>
</FormGroup>
</div>}
</div>
</Form>
</div>
</div>
{/* Save UI */}
<div
id="pinboard-controls"
className="btn-group-vertical"
title={this.hasPinnedJobs() ? '' : 'No pinned jobs'}
>
<div className="btn-group save-btn-group dropdown">
<button
className={`btn btn-light-bordered btn-xs save-btn ${!isLoggedIn || !this.canSaveClassifications() ? 'disabled' : ''}`}
title={this.saveUITitle('classification')}
onClick={this.save}
>save
</button>
<button
className={`btn btn-light-bordered btn-xs dropdown-toggle save-btn-dropdown ${!this.hasPinnedJobs() && !this.pinboardIsDirty() ? 'disabled' : ''}`}
title={!this.hasPinnedJobs() && !this.pinboardIsDirty() ? 'No pinned jobs' : 'Additional pinboard functions'}
type="button"
data-toggle="dropdown"
>
<span className="caret" />
</button>
<ul className="dropdown-menu save-btn-dropdown-menu">
<li
className={!isLoggedIn ? 'disabled' : ''}
title={!isLoggedIn ? 'Not logged in' : 'Repeat the pinned jobs'}
>
<a
className="dropdown-item"
onClick={() => !isLoggedIn || this.retriggerAllPinnedJobs()}
>Retrigger all</a></li>
<li
className={this.canCancelAllPinnedJobs() ? '' : 'disabled'}
title={this.cancelAllPinnedJobsTitle()}
>
<a
className="dropdown-item"
onClick={() => this.canCancelAllPinnedJobs() && this.cancelAllPinnedJobs()}
>Cancel all</a>
</li>
<li><a className="dropdown-item" onClick={() => this.unPinAll()}>Clear
all</a></li>
</ul>
</div>
</div>
</div>
</div>
);
}
}
PinBoard.propTypes = {
$injector: PropTypes.object.isRequired,
classificationTypes: PropTypes.object.isRequired,
isLoggedIn: PropTypes.bool.isRequired,
isVisible: PropTypes.bool.isRequired,
pinnedJobs: PropTypes.object.isRequired,
pinnedJobBugs: PropTypes.object.isRequired,
addBug: PropTypes.func.isRequired,
removeBug: PropTypes.func.isRequired,
unPinJob: PropTypes.func.isRequired,
pinJob: PropTypes.func.isRequired,
unPinAll: PropTypes.func.isRequired,
selectedJob: PropTypes.object,
email: PropTypes.string,
revisionList: PropTypes.array,
};
PinBoard.defaultProps = {
selectedJob: null,
email: null,
revisionList: [],
};

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

@ -0,0 +1,406 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Queue, slugid } from 'taskcluster-client-web';
import $ from 'jquery';
import jsyaml from 'js-yaml';
import thTaskcluster from '../../../js/services/taskcluster';
import tcJobActionsTemplate from '../../../partials/main/tcjobactions.html';
import { thEvents } from '../../../js/constants';
import { formatModelError, formatTaskclusterError } from '../../../helpers/errorMessage';
import { isReftest } from '../../../helpers/job';
import { getInspectTaskUrl, getReftestUrl } from '../../../helpers/url';
import JobDetailModel from '../../../models/jobDetail';
import JobModel from '../../../models/job';
import LogUrls from './LogUrls';
export default class ActionBar extends React.Component {
constructor(props) {
super(props);
const { $injector } = this.props;
this.thNotify = $injector.get('thNotify');
this.thBuildApi = $injector.get('thBuildApi');
this.ThResultSetStore = $injector.get('ThResultSetStore');
this.tcactions = $injector.get('tcactions');
this.$interpolate = $injector.get('$interpolate');
this.$uibModal = $injector.get('$uibModal');
this.$rootScope = $injector.get('$rootScope');
this.$timeout = $injector.get('$timeout');
}
componentDidMount() {
const { logParseStatus } = this.props;
// Open the logviewer and provide notifications if it isn't available
this.openLogViewerUnlisten = this.$rootScope.$on(thEvents.openLogviewer, () => {
switch (logParseStatus) {
case 'pending':
this.thNotify.send('Log parsing in progress, log viewer not yet available', 'info'); break;
case 'failed':
this.thNotify.send('Log parsing has failed, log viewer is unavailable', 'warning'); break;
case 'unavailable':
this.thNotify.send('No logs available for this job', 'info'); break;
case 'parsed':
$('.logviewer-btn')[0].click();
}
});
this.jobRetriggerUnlisten = this.$rootScope.$on(thEvents.jobRetrigger, (event, job) => {
this.retriggerJob([job]);
});
this.customJobAction = this.customJobAction.bind(this);
}
componentWillUnmount() {
this.openLogViewerUnlisten();
this.jobRetriggerUnlisten();
}
canCancel() {
const { selectedJob } = this.props;
return selectedJob.state === 'pending' || selectedJob.state === 'running';
}
retriggerJob(jobs) {
const { user, repoName } = this.props;
if (user.isLoggedIn) {
// Spin the retrigger button when retriggers happen
$('#retrigger-btn > span').removeClass('action-bar-spin');
window.requestAnimationFrame(function () {
window.requestAnimationFrame(function () {
$('#retrigger-btn > span').addClass('action-bar-spin');
});
});
const job_id_list = jobs.map(job => job.id);
// The logic here is somewhat complicated because we need to support
// two use cases the first is the case where we notify a system other
// then buildbot that a retrigger has been requested (eg mozilla-taskcluster).
// The second is when we have the buildapi id and need to send a request
// to the self serve api (which does not listen over pulse!).
JobModel.retrigger(repoName, job_id_list).then(() => (
JobDetailModel.getJobDetails({
title: 'buildbot_request_id',
repository: repoName,
job_id__in: job_id_list.join(',') })
.then((data) => {
const requestIdList = data.map(datum => datum.value);
requestIdList.forEach((requestId) => {
this.thBuildApi.retriggerJob(repoName, requestId);
});
})
).then(() => {
this.$timeout(this.thNotify.send('Retrigger request sent', 'success'));
}, (e) => {
// Generic error eg. the user doesn't have LDAP access
this.$timeout(this.thNotify.send(
formatModelError(e, 'Unable to send retrigger'), 'danger'));
}));
} else {
this.$timeout(this.thNotify.send('Must be logged in to retrigger a job', 'danger'));
}
}
backfillJob() {
const { user, selectedJob, repoName } = this.props;
if (!this.canBackfill()) {
return;
}
if (!user.isLoggedIn) {
this.thNotify.send('Must be logged in to backfill a job', 'danger');
return;
}
if (!selectedJob.id) {
this.thNotify.send('Job not yet loaded for backfill', 'warning');
return;
}
if (selectedJob.build_system_type === 'taskcluster' || selectedJob.reason.startsWith('Created by BBB for task')) {
this.ThResultSetStore.getGeckoDecisionTaskId(
selectedJob.result_set_id).then(function (decisionTaskId) {
return this.tcactions.load(decisionTaskId, selectedJob).then((results) => {
const actionTaskId = slugid();
if (results) {
const backfilltask = results.actions.find(result => result.name === 'backfill');
// We'll fall back to actions.yaml if this isn't true
if (backfilltask) {
return this.tcactions.submit({
action: backfilltask,
actionTaskId,
decisionTaskId,
taskId: results.originalTaskId,
task: results.originalTask,
input: {},
staticActionVariables: results.staticActionVariables,
}).then(() => {
this.$timeout(() => this.thNotify.send(
`Request sent to backfill job via actions.json (${actionTaskId})`,
'success')
);
}, (e) => {
// The full message is too large to fit in a Treeherder
// notification box.
this.$timeout(() => this.thNotify.send(
formatTaskclusterError(e),
'danger',
{ sticky: true })
);
});
}
}
// Otherwise we'll figure things out with actions.yml
const queue = new Queue({ credentialAgent: thTaskcluster.getAgent() });
// buildUrl is documented at
// https://github.com/taskcluster/taskcluster-client-web#construct-urls
// It is necessary here because getLatestArtifact assumes it is getting back
// JSON as a reponse due to how the client library is constructed. Since this
// result is yml, we'll fetch it manually using $http and can use the url
// returned by this method.
const url = queue.buildUrl(
queue.getLatestArtifact,
decisionTaskId,
'public/action.yml'
);
fetch(url).then((resp) => {
let action = resp.data;
const template = this.$interpolate(action);
action = template({
action: 'backfill',
action_args: `--project=${repoName}' --job=${selectedJob.id}`,
});
const task = thTaskcluster.refreshTimestamps(jsyaml.safeLoad(action));
queue.createTask(actionTaskId, task).then(function () {
this.$timeout(() => this.thNotify.send(
`Request sent to backfill job via actions.yml (${actionTaskId})`,
'success')
);
}, (e) => {
// The full message is too large to fit in a Treeherder
// notification box.
this.$timeout(() => this.thNotify.send(
formatTaskclusterError(e),
'danger',
{ sticky: true })
);
});
});
});
});
} else {
this.thNotify.send('Unable to backfill this job type!', 'danger', { sticky: true });
}
}
// Can we backfill? At the moment, this only ensures we're not in a 'try' repo.
canBackfill() {
const { user, isTryRepo } = this.props;
return user.isLoggedIn && !isTryRepo;
}
backfillButtonTitle() {
const { user, isTryRepo } = this.props;
let title = '';
if (!user.isLoggedIn) {
title = title.concat('must be logged in to backfill a job / ');
}
if (isTryRepo) {
title = title.concat('backfill not available in this repository');
}
if (title === '') {
title = 'Trigger jobs of ths type on prior pushes ' +
'to fill in gaps where the job was not run';
} else {
// Cut off trailing '/ ' if one exists, capitalize first letter
title = title.replace(/\/ $/, '');
title = title.replace(/^./, l => l.toUpperCase());
}
return title;
}
cancelJobs(jobs) {
const { repoName } = this.props;
const jobIdsToCancel = jobs.filter(job => (job.state === 'pending' ||
job.state === 'running')).map(
job => job.id);
// get buildbot ids of any buildbot jobs we want to cancel
// first
JobDetailModel.getJobDetails({
job_id__in: jobIdsToCancel,
title: 'buildbot_request_id'
}).then(buildbotRequestIdDetails => (
JobModel.cancel(repoName, jobIdsToCancel).then(
() => {
buildbotRequestIdDetails.forEach(
(buildbotRequestIdDetail) => {
const requestId = parseInt(buildbotRequestIdDetail.value);
this.thBuildApi.cancelJob(repoName, requestId);
});
})
)).then(() => {
this.thNotify.send('Cancel request sent', 'success');
}).catch(function (e) {
this.thNotify.send(
formatModelError(e, 'Unable to cancel job'),
'danger',
{ sticky: true }
);
});
}
cancelJob() {
this.cancelJobs([this.props.selectedJob]);
}
customJobAction() {
const { repoName, selectedJob } = this.props;
this.$uibModal.open({
template: tcJobActionsTemplate,
controller: 'TCJobActionsCtrl',
size: 'lg',
resolve: {
job: () => selectedJob,
repoName: () => repoName,
resultsetId: () => selectedJob.result_set_id
}
});
}
render() {
const { selectedJob, logViewerUrl, logViewerFullUrl, jobLogUrls, user, pinJob } = this.props;
return (
<div id="job-details-actionbar">
<nav className="navbar navbar-dark details-panel-navbar">
<ul className="nav navbar-nav actionbar-nav">
<LogUrls
logUrls={jobLogUrls}
logViewerUrl={logViewerUrl}
logViewerFullUrl={logViewerFullUrl}
/>
<li>
<span
id="pin-job-btn"
title="Add this job to the pinboard"
className="btn icon-blue"
onClick={() => pinJob(selectedJob)}
><span className="fa fa-thumb-tack" /></span>
</li>
<li>
<span
id="retrigger-btn"
title={user.isLoggedIn ? 'Repeat the selected job' : 'Must be logged in to retrigger a job'}
className={`btn ${user.isLoggedIn ? 'icon-green' : 'disabled'}`}
disabled={!user.isLoggedIn}
onClick={() => this.retriggerJob([selectedJob])}
><span className="fa fa-repeat" /></span>
</li>
{isReftest(selectedJob) && jobLogUrls.map(jobLogUrl => (<li key={`reftest-${jobLogUrl.id}`}>
<a
title="Launch the Reftest Analyser in a new window"
target="_blank"
rel="noopener"
href={getReftestUrl(jobLogUrl)}
><span className="fa fa-bar-chart-o" /></a>
</li>))}
{this.canCancel() && <li>
<a
title={user.isLoggedIn ? 'Cancel this job' : 'Must be logged in to cancel a job'}
className={user.isLoggedIn ? 'hover-warning' : 'disabled'}
onClick={() => this.cancelJob()}
><span className="fa fa-times-circle cancel-job-icon" /></a>
</li>}
</ul>
<ul className="nav navbar-right">
<li className="dropdown">
<span
id="actionbar-menu-btn"
title="Other job actions"
aria-haspopup="true"
aria-expanded="false"
className="dropdown-toggle"
data-toggle="dropdown"
><span className="fa fa-ellipsis-h" aria-hidden="true" /></span>
<ul className="dropdown-menu actionbar-menu" role="menu">
<li>
<span
id="backfill-btn"
className={`btn dropdown-item ${!user.isLoggedIn || !this.canBackfill() ? 'disabled' : ''}`}
title={this.backfillButtonTitle()}
onClick={() => !this.canBackfill() || this.backfillJob()}
>Backfill</span>
</li>
{selectedJob.taskcluster_metadata && <React.Fragment>
<li>
<a
target="_blank"
rel="noopener"
className="dropdown-item"
href={getInspectTaskUrl(selectedJob.taskcluster_metadata.task_id)}
>Inspect Task</a>
</li>
<li>
<a
target="_blank"
rel="noopener"
className="dropdown-item"
href={`${getInspectTaskUrl(selectedJob.taskcluster_metadata.task_id)}/create`}
>Edit and Retrigger</a>
</li>
<li>
<a
target="_blank"
rel="noopener"
className="dropdown-item"
href={`https://tools.taskcluster.net/tasks/${selectedJob.taskcluster_metadata.task_id}/interactive`}
>Create Interactive Task</a>
</li>
<li>
<a
onClick={this.customJobAction}
className="dropdown-item"
>Custom Action...</a>
</li>
</React.Fragment>}
</ul>
</li>
</ul>
</nav>
</div>
);
}
}
ActionBar.propTypes = {
pinJob: PropTypes.func.isRequired,
$injector: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
repoName: PropTypes.string.isRequired,
selectedJob: PropTypes.object.isRequired,
logParseStatus: PropTypes.string.isRequired,
jobLogUrls: PropTypes.array,
isTryRepo: PropTypes.bool,
logViewerUrl: PropTypes.string,
logViewerFullUrl: PropTypes.string,
};
ActionBar.defaultProps = {
isTryRepo: true, // default to more restrictive for backfilling
logViewerUrl: null,
logViewerFullUrl: null,
jobLogUrls: [],
};

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

@ -1,36 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getBugUrl, linkifyRevisions } from '../../../helpers/url';
import { linkifyRevisions, getBugUrl } from '../../../helpers/url';
export default function ClassificationsPanel(props) {
const {
dateFilter, repoName, ThRepositoryModel,
classification, job, classificationTypes, bugs,
$injector, repoName, classification, job, bugs,
} = props;
const ThRepositoryModel = $injector.get('ThRepositoryModel');
const dateFilter = $injector.get('dateFilter');
const classificationTypes = $injector.get('thClassificationTypes');
const repo = ThRepositoryModel.getRepo(repoName);
const repoURLHTML = { __html: linkifyRevisions(classification.text, repo) };
const failureId = classification.failure_classification_id;
const iconClass = (failureId === 7 ?
'fa-star-o' : 'fa fa-star') + ' star-' + job.result;
const iconClass = `${(failureId === 7 ? 'fa-star-o' : 'fa fa-star')} star-${job.result}`;
const classificationName = classificationTypes.classifications[failureId];
return (
<ul className="list-unstyled content-spacer">
<React.Fragment>
<li>
<span title={classificationName.name}>
<i className={`fa ${iconClass}`} />
<span className="ml-1">{classificationName.name}</span>
</span>
{!!bugs.length &&
<a
target="_blank"
rel="noopener"
href={getBugUrl(bugs[0].bug_id)}
title={`View bug ${bugs[0].bug_id}`}
><em> {bugs[0].bug_id}</em></a>}
<a
target="_blank"
rel="noopener"
href={getBugUrl(bugs[0].bug_id)}
title={`View bug ${bugs[0].bug_id}`}
><em> {bugs[0].bug_id}</em></a>}
</li>
{classification.text.length > 0 &&
<li><em dangerouslySetInnerHTML={repoURLHTML} /></li>
<li><em dangerouslySetInnerHTML={repoURLHTML} /></li>
}
<li className="revision-comment">
{dateFilter(classification.created, 'EEE MMM d, H:mm:ss')}
@ -38,21 +42,14 @@ export default function ClassificationsPanel(props) {
<li className="revision-comment">
{classification.who}
</li>
</ul>
</React.Fragment>
);
}
ClassificationsPanel.propTypes = {
dateFilter: PropTypes.func.isRequired,
$injector: PropTypes.object.isRequired,
repoName: PropTypes.string.isRequired,
ThRepositoryModel: PropTypes.object.isRequired,
classification: PropTypes.object.isRequired,
job: PropTypes.object.isRequired,
classificationTypes: PropTypes.object.isRequired,
bugs: PropTypes.array,
bugs: PropTypes.array.isRequired,
};
ClassificationsPanel.defaultProps = {
bugs: [],
};

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

@ -0,0 +1,89 @@
import React from 'react';
import PropTypes from 'prop-types';
import logviewerIcon from '../../../img/logviewerIcon.svg';
function getLogUrlProps(logUrl, logViewerUrl, logViewerFullUrl) {
switch (logUrl.parse_status) {
case 'parsed':
return {
target: '_blank',
rel: 'noopener',
href: logViewerUrl,
'copy-value': logViewerFullUrl,
title: 'Open the log viewer in a new window'
};
case 'failed':
return {
className: 'disabled',
title: 'Log parsing has failed',
};
case 'pending':
return {
className: 'disabled',
title: 'Log parsing in progress'
};
}
}
export default function LogUrls(props) {
const { logUrls, logViewerUrl, logViewerFullUrl } = props;
return (
<React.Fragment>
{logUrls.map(jobLogUrl => (<li key={`logview-${jobLogUrl.id}`}>
<a
className="logviewer-btn"
{...getLogUrlProps(jobLogUrl, logViewerUrl, logViewerFullUrl)}
>
<img
alt="Logviewer"
src={logviewerIcon}
className="logviewer-icon"
/>
</a>
</li>))}
<li>
{!logUrls.length && <a
className="logviewer-btn disabled"
title="No logs available for this job"
>
<img
alt="Logviewer"
src={logviewerIcon}
className="logviewer-icon"
/>
</a>}
</li>
{logUrls.map(jobLogUrl => (<li key={`raw-${jobLogUrl.id}`}>
<a
id="raw-log-btn"
className="raw-log-icon"
title="Open the raw log in a new window"
target="_blank"
rel="noopener"
href={jobLogUrl.url}
copy-value={jobLogUrl.url}
><span className="fa fa-file-text-o" /></a>
</li>))}
{!logUrls.length && <li>
<a
className="disabled raw-log-icon"
title="No logs available for this job"
><span className="fa fa-file-text-o" /></a>
</li>}
</React.Fragment>
);
}
LogUrls.propTypes = {
logUrls: PropTypes.array.isRequired,
logViewerUrl: PropTypes.string,
logViewerFullUrl: PropTypes.string,
};
LogUrls.defaultProps = {
logViewerUrl: null,
logViewerFullUrl: null,
};

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

@ -4,29 +4,27 @@ import PropTypes from 'prop-types';
import { getStatus } from '../../../helpers/job';
export default function StatusPanel(props) {
const { job } = props;
const shadingClass = `result-status-shading-${getStatus(job)}`;
const { selectedJob } = props;
const shadingClass = `result-status-shading-${getStatus(selectedJob)}`;
return (
<ul className="list-unstyled">
<li
id="result-status-pane"
className={`small ${shadingClass}`}
>
<div>
<label>Result:</label>
<span> {job.result}</span>
</div>
<div>
<label>State:</label>
<span> {job.state}</span>
</div>
</li>
</ul>
<li
id="result-status-pane"
className={`small ${shadingClass}`}
>
<div>
<label>Result:</label>
<span> {selectedJob.result}</span>
</div>
<div>
<label>State:</label>
<span> {selectedJob.state}</span>
</div>
</li>
);
}
StatusPanel.propTypes = {
job: PropTypes.object.isRequired,
selectedJob: PropTypes.object.isRequired,
};

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

@ -1,83 +1,14 @@
import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import treeherder from '../../../js/treeherder';
import {
getSlaveHealthUrl,
getInspectTaskUrl,
getWorkerExplorerUrl,
getJobSearchStrHref,
} from '../../../helpers/url';
import { getSearchStr } from '../../../helpers/job';
import { toDateStr } from '../../../helpers/display';
import { getSearchStr, getTimeFields, getJobMachineUrl } from '../../../helpers/job';
import { getInspectTaskUrl, getJobSearchStrHref } from '../../../helpers/url';
import ActionBar from './ActionBar';
import ClassificationsPanel from './ClassificationsPanel';
import StatusPanel from './StatusPanel';
function JobDetailsListItem(props) {
const {
label, labelHref, labelTitle, labelOnclick, labelTarget, labelText,
href, text, title, onclick, target, iconClass
} = props;
return (
<li className="small">
<label>{label}</label>
{labelHref &&
<a
title={labelTitle}
href={labelHref}
onClick={labelOnclick}
target={labelTarget}
rel="noopener"
>{labelText} <span className="fa fa-pencil-square-o icon-superscript" />: </a>
}
{!href ? <span className="ml-1">{text}</span> :
<a
title={title}
className="ml-1"
href={href}
onClick={onclick}
target={target}
rel="noopener"
>{text}</a>
}
{iconClass && <span className={`ml-1${iconClass}`} />}
</li>
);
}
JobDetailsListItem.propTypes = {
label: PropTypes.string.isRequired,
labelHref: PropTypes.string,
labelTitle: PropTypes.string,
labelText: PropTypes.string,
href: PropTypes.string,
text: PropTypes.string,
title: PropTypes.string,
target: PropTypes.string,
iconClass: PropTypes.string,
labelOnclick: PropTypes.func,
labelTarget: PropTypes.string,
onclick: PropTypes.func,
};
JobDetailsListItem.defaultProps = {
labelHref: null,
labelTitle: null,
labelText: null,
href: null,
text: null,
title: null,
target: null,
iconClass: null,
labelOnclick: null,
labelTarget: null,
onclick: null,
};
class JobDetailsList extends React.Component {
export default class SummaryPanel extends React.Component {
constructor(props) {
super(props);
@ -87,7 +18,7 @@ class JobDetailsList extends React.Component {
}
componentWillReceiveProps(nextProps) {
if (_.isEmpty(nextProps.job)) {
if (!nextProps.selectedJob || !Object.keys(nextProps.selectedJob).length) {
return;
}
@ -98,7 +29,7 @@ class JobDetailsList extends React.Component {
let machineUrl = null;
try {
machineUrl = await this.getJobMachineUrl(props);
machineUrl = await getJobMachineUrl(props);
} catch (err) {
machineUrl = '';
}
@ -108,217 +39,164 @@ class JobDetailsList extends React.Component {
}
}
getJobMachineUrl(props) {
const { job } = props;
const { build_system_type, machine_name } = job;
const machineUrl = (machine_name !== 'unknown' && build_system_type === 'buildbot') ?
getSlaveHealthUrl(machine_name) :
getWorkerExplorerUrl(job.taskcluster_metadata.task_id);
return machineUrl;
}
getTimeFields(job) {
// time fields to show in detail panel, but that should be grouped together
const timeFields = {
requestTime: toDateStr(job.submit_timestamp)
};
// If start time is 0, then duration should be from requesttime to now
// If we have starttime and no endtime, then duration should be starttime to now
// If we have both starttime and endtime, then duration will be between those two
const endtime = job.end_timestamp || Date.now() / 1000;
const starttime = job.start_timestamp || job.submit_timestamp;
const duration = `${Math.round((endtime - starttime)/60, 0)} minute(s)`;
if (job.start_timestamp) {
timeFields.startTime = toDateStr(job.start_timestamp);
timeFields.duration = duration;
} else {
timeFields.duration = `Not started (queued for ${duration})`;
}
if (job.end_timestamp) {
timeFields.endTime = toDateStr(job.end_timestamp);
}
return timeFields;
}
render() {
const { job, jobLogUrls } = this.props;
const timeFields = this.getTimeFields(job);
const jobMachineName = job.machine_name;
const jobSearchStr = getSearchStr(job);
let buildUrl = null;
let iconCircleClass = null;
if (job.build_system_type === 'buildbot' && !!jobLogUrls.length) {
buildUrl = jobLogUrls[0].buildUrl;
}
if (job.job_type_description) {
iconCircleClass = 'fa fa-info-circle';
}
return (
<ul className="list-unstyled content-spacer">
<JobDetailsListItem
label="Job"
labelTitle="Filter jobs with this unique SHA signature"
labelHref={getJobSearchStrHref(job.signature)}
labelText="(sig)"
title="Filter jobs containing these keywords"
href={getJobSearchStrHref(jobSearchStr)}
text={jobSearchStr}
/>
{jobMachineName &&
<JobDetailsListItem
label="Machine: "
text={jobMachineName}
title="Inspect machine"
target="_blank"
href={this.state.machineUrl}
/>
}
{job.taskcluster_metadata &&
<JobDetailsListItem
label="Task:"
text={job.taskcluster_metadata.task_id}
href={getInspectTaskUrl(job.taskcluster_metadata.task_id)}
target="_blank"
/>
}
<JobDetailsListItem
key="Build"
label={`Build:`}
title="Open build directory in a new tab"
href={buildUrl}
target="_blank"
text={`${job.build_architecture} ${job.build_platform} ${job.build_os || ''}`}
iconClass={iconCircleClass}
/>
<JobDetailsListItem
key="Job name"
label="Job name:"
title="Open build directory in a new tab"
href={buildUrl}
target="_blank"
text={job.job_type_name}
iconClass={iconCircleClass}
/>
{timeFields && <span>
<JobDetailsListItem
label="Requested:"
text={timeFields.requestTime}
/>
{timeFields.startTime &&
<JobDetailsListItem
label="Started:"
text={timeFields.startTime}
/>
}
{timeFields.endTime &&
<JobDetailsListItem
label="Ended:"
text={timeFields.endTime}
/>
}
<JobDetailsListItem
label="Duration:"
text={timeFields.duration}
/>
</span>}
{!jobLogUrls.length ?
<JobDetailsListItem label="Log parsing status: " text="No logs" /> :
jobLogUrls.map(data => (
<JobDetailsListItem
label="Log parsing status: "
text={data.parse_status}
key={data}
/>
))
}
</ul>
);
}
}
JobDetailsList.propTypes = {
job: PropTypes.object.isRequired,
jobLogUrls: PropTypes.array,
};
JobDetailsList.defaultProps = {
jobLogUrls: [],
};
class SummaryPanel extends React.Component {
constructor(props) {
super(props);
const { $injector } = this.props;
this.dateFilter = $injector.get('$filter')('date');
this.ThRepositoryModel = $injector.get('ThRepositoryModel');
}
render() {
const {
jobDetailLoading, job, classificationTypes, repoName,
jobLogUrls, buildUrl, classification, bugs
repoName, selectedJob, latestClassification, bugs, jobLogUrls,
jobDetailLoading, buildUrl, logViewerUrl, logViewerFullUrl, isTryRepo, logParseStatus,
pinJob, $injector, user,
} = this.props;
const timeFields = getTimeFields(selectedJob);
const jobMachineName = selectedJob.machine_name;
const jobSearchStr = getSearchStr(selectedJob);
let iconCircleClass = null;
const buildDirectoryUrl = (selectedJob.build_system_type === 'buildbot' && !!jobLogUrls.length) ?
jobLogUrls[0].buildUrl : buildUrl;
if (selectedJob.job_type_description) {
iconCircleClass = 'fa fa-info-circle';
}
return (
<div>
{jobDetailLoading &&
<div className="overlay">
<div>
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
</div>
</div>
}
{classification &&
<ClassificationsPanel
job={job}
classification={classification}
bugs={bugs}
dateFilter={this.dateFilter}
classificationTypes={classificationTypes}
repoName={repoName}
ThRepositoryModel={this.ThRepositoryModel}
/>
}
<StatusPanel
job={job}
/>
<JobDetailsList
job={job}
<div id="summary-panel">
<ActionBar
repoName={repoName}
selectedJob={selectedJob}
logParseStatus={logParseStatus}
isTryRepo={isTryRepo}
logViewerUrl={logViewerUrl}
logViewerFullUrl={logViewerFullUrl}
jobLogUrls={jobLogUrls}
buildUrl={buildUrl}
pinJob={pinJob}
$injector={$injector}
user={user}
/>
<div id="summary-panel-content">
<div>
{jobDetailLoading &&
<div className="overlay">
<div>
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
</div>
</div>
}
<ul className="list-unstyled">
{latestClassification &&
<ClassificationsPanel
job={selectedJob}
classification={latestClassification}
bugs={bugs}
repoName={repoName}
$injector={$injector}
/>}
<StatusPanel selectedJob={selectedJob} />
<div>
<li className="small">
<label title="">Job</label>
<a
title="Filter jobs with this unique SHA signature"
href={getJobSearchStrHref(selectedJob.signature)}
>(sig)</a>:&nbsp;
<a
title="Filter jobs containing these keywords"
href={getJobSearchStrHref(jobSearchStr)}
>{jobSearchStr}</a>
</li>
{jobMachineName &&
<li className="small">
<label>Machine: </label>
<a
title="Inspect machine"
target="_blank"
href={this.state.machineUrl}
>{jobMachineName}</a>
</li>
}
{selectedJob.taskcluster_metadata &&
<li className="small">
<label>Task: </label>
<a
href={getInspectTaskUrl(selectedJob.taskcluster_metadata.task_id)}
target="_blank"
>{selectedJob.taskcluster_metadata.task_id}</a>
</li>
}
<li className="small">
<label>Build: </label>
<a
title="Open build directory in a new tab"
href={buildUrl}
target="_blank"
>{`${selectedJob.build_architecture} ${selectedJob.build_platform} ${selectedJob.build_os || ''}`}</a>
<span className={`ml-1${iconCircleClass}`} />
</li>
<li className="small">
<label>Job name: </label>
<a
title="Open build directory in a new tab"
href={buildDirectoryUrl}
target="_blank"
>{selectedJob.job_type_name}</a>
<span className={`ml-1${iconCircleClass}`} />
</li>
{timeFields && <span>
<li className="small">
<label>Requested: </label>{timeFields.requestTime}
</li>
{timeFields.startTime && <li className="small">
<label>Started: </label>{timeFields.startTime}
</li>}
{timeFields.endTime && <li className="small">
<label>Ended: </label>{timeFields.endTime}
</li>}
<li className="small">
<label>Duration: </label>{timeFields.duration}
</li>
</span>}
{!jobLogUrls.length ?
<li className="small"><label>Log parsing status: </label>No logs</li> :
jobLogUrls.map(data => (
<li className="small" key={data}>
<label>Log parsing status: </label>{data.parse_status}
</li>
))
}
</div>
</ul>
</div>
</div>
</div>
);
}
}
SummaryPanel.propTypes = {
job: PropTypes.object.isRequired,
$injector: PropTypes.object.isRequired,
classificationTypes: PropTypes.object.isRequired,
repoName: PropTypes.string.isRequired,
pinJob: PropTypes.func.isRequired,
bugs: PropTypes.array.isRequired,
$injector: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
selectedJob: PropTypes.object,
latestClassification: PropTypes.object,
jobLogUrls: PropTypes.array,
jobDetailLoading: PropTypes.bool,
classification: PropTypes.object,
bugs: PropTypes.array,
buildUrl: PropTypes.string,
logParseStatus: PropTypes.string,
isTryRepo: PropTypes.bool,
logViewerUrl: PropTypes.string,
logViewerFullUrl: PropTypes.string,
};
SummaryPanel.defaultProps = {
selectedJob: null,
latestClassification: null,
jobLogUrls: [],
jobDetailLoading: false,
classification: null,
bugs: [],
buildUrl: null,
logParseStatus: 'pending',
isTryRepo: true,
logViewerUrl: null,
logViewerFullUrl: null,
};
treeherder.component('summaryPanel', react2angular(
SummaryPanel,
['job', 'classificationTypes', 'repoName', 'jobLogUrls', 'jobDetailLoading', 'classification', 'bugs', 'buildUrl'],
['$injector']));

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

@ -1,10 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import treeherder from '../../../js/treeherder';
import { thEvents } from "../../../js/constants";
import { getBugUrl } from '../../../helpers/url';
import { thEvents } from '../../../js/constants';
function RelatedBugSaved(props) {
const { deleteBug, bug } = props;
@ -48,7 +46,7 @@ function RelatedBug(props) {
<li key={bug.bug_id}>
<RelatedBugSaved
bug={bug}
deleteBug={deleteBug}
deleteBug={() => deleteBug(bug)}
/>
</li>))}
</ul>
@ -144,17 +142,19 @@ export default class AnnotationsTab extends React.Component {
super(props);
const { $injector } = props;
this.$rootScope = $injector.get('$rootScope');
this.thNotify = $injector.get('thNotify');
this.ThResultSetStore = $injector.get('ThResultSetStore');
this.$rootScope = $injector.get('$rootScope');
this.deleteBug = this.deleteBug.bind(this);
this.deleteClassification = this.deleteClassification.bind(this);
}
componentDidMount() {
const { classifications, bugs } = this.props;
this.$rootScope.$on(thEvents.deleteClassification, () => {
if (classifications[0]) {
this.deleteClassificationUnlisten = this.$rootScope.$on(thEvents.deleteClassification, () => {
if (classifications.length) {
this.deleteClassification(classifications[0]);
// Delete any number of bugs if they exist
bugs.forEach((bug) => { this.deleteBug(bug); });
@ -162,9 +162,10 @@ export default class AnnotationsTab extends React.Component {
this.thNotify.send('No classification on this job to delete', 'warning');
}
});
}
this.deleteBug = this.deleteBug.bind(this);
this.deleteClassification = this.deleteClassification.bind(this);
componentWillUnmount() {
this.deleteClassificationUnlisten();
}
deleteClassification(classification) {
@ -199,22 +200,11 @@ export default class AnnotationsTab extends React.Component {
bug.destroy()
.then(() => {
this.thNotify.send(
`Association to bug ${bug.bug_id} successfully deleted`,
'success'
);
this.$rootScope.$emit(
thEvents.bugsAssociated,
{ jobs: { [selectedJob.id]: selectedJob } }
);
this.thNotify.send(`Association to bug ${bug.bug_id} successfully deleted`, 'success');
this.$rootScope.$emit(thEvents.bugsAssociated, { jobs: { [selectedJob.id]: selectedJob } });
}, () => {
this.thNotify.send(
`Association to bug ${bug.bug_id} deletion failed`,
'danger',
{ sticky: true }
);
}
);
this.thNotify.send(`Association to bug ${bug.bug_id} deletion failed`, 'danger', { sticky: true });
});
}
render() {
@ -227,7 +217,7 @@ export default class AnnotationsTab extends React.Component {
return (
<div className="container-fluid">
<div className="row h-100">
<div className="col-sm-10 classifications-pane job-tabs-content">
<div className="col-sm-10 classifications-pane">
{classifications.length ?
<AnnotationsTable
classifications={classifications}
@ -255,18 +245,11 @@ export default class AnnotationsTab extends React.Component {
AnnotationsTab.propTypes = {
$injector: PropTypes.object.isRequired,
classificationTypes: PropTypes.object.isRequired,
classifications: PropTypes.array,
bugs: PropTypes.array,
bugs: PropTypes.array.isRequired,
classifications: PropTypes.array.isRequired,
selectedJob: PropTypes.object,
};
AnnotationsTab.defaultProps = {
classifications: [],
bugs: [],
selectedJob: null,
};
treeherder.component('annotationsTab', react2angular(
AnnotationsTab,
['classificationTypes', 'classifications', 'bugs', 'selectedJob'],
['$injector']));

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

@ -1,19 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import treeherder from '../../../js/treeherder';
import { getPerfAnalysisUrl, getWptUrl } from '../../../helpers/url';
export default class JobDetailsTab extends React.PureComponent {
render() {
const { jobDetails, buildernameIndex } = this.props;
const { jobDetails } = this.props;
const sortedDetails = jobDetails ? jobDetails.slice() : [];
const builderNameItem = jobDetails.findIndex(detail => detail.title === "Buildername");
sortedDetails.sort((a, b) => a.title.localeCompare(b.title));
return (
<div className="job-tabs-content">
<div id="job-details-list">
<ul className="list-unstyled">
{sortedDetails.map((line, idx) => (
<li
@ -31,7 +29,7 @@ export default class JobDetailsTab extends React.PureComponent {
{line.url && line.value.endsWith('raw.log') &&
<span> - <a
title={line.value}
href={getWptUrl(line.url, jobDetails[buildernameIndex] ? jobDetails[buildernameIndex].value : undefined)}
href={getWptUrl(line.url, builderNameItem ? builderNameItem.value : undefined)}
>open in test results viewer</a>
</span>}
{line.url && line.value.startsWith('profile_') && line.value.endsWith('.zip') &&
@ -57,12 +55,8 @@ export default class JobDetailsTab extends React.PureComponent {
JobDetailsTab.propTypes = {
jobDetails: PropTypes.array,
buildernameIndex: PropTypes.number,
};
JobDetailsTab.defaultProps = {
jobDetails: [],
buildernameIndex: null,
};
treeherder.component('jobDetailsTab', react2angular(JobDetailsTab));

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

@ -1,11 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import treeherder from '../../../js/treeherder';
import { getCompareChooserUrl } from '../../../helpers/url';
class PerformanceTab extends React.PureComponent {
export default class PerformanceTab extends React.PureComponent {
render() {
const { repoName, revision, perfJobDetail } = this.props;
@ -52,7 +50,3 @@ PerformanceTab.defaultProps = {
perfJobDetail: [],
revision: '',
};
treeherder.component('performanceTab', react2angular(
PerformanceTab,
['repoName', 'revision', 'perfJobDetail']));

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

@ -1,16 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import { getBtnClass, getStatus } from '../../../helpers/job';
import { toDateStr, toShortDateStr } from '../../../helpers/display';
import { getBtnClass, getStatus } from '../../../helpers/job';
import { getSlaveHealthUrl, getJobsUrl } from '../../../helpers/url';
import treeherder from '../../../js/treeherder';
import { thEvents } from '../../../js/constants';
import JobModel from '../../../models/job';
import TextLogStepModel from '../../../models/textLogStep';
class SimilarJobsTab extends React.Component {
export default class SimilarJobsTab extends React.Component {
constructor(props) {
super(props);
@ -18,11 +15,9 @@ class SimilarJobsTab extends React.Component {
this.$rootScope = $injector.get('$rootScope');
this.ThResultSetModel = $injector.get('ThResultSetModel');
this.thNotify = $injector.get('thNotify');
this.thTabs = $injector.get('thTabs');
this.thClassificationTypes = $injector.get('thClassificationTypes');
this.pageSize = 20;
this.tab = this.thTabs.tabs.similarJobs;
this.state = {
similarJobs: [],
@ -31,7 +26,6 @@ class SimilarJobsTab extends React.Component {
page: 1,
selectedSimilarJob: null,
hasNextPage: false,
selectedJob: null,
isLoading: true,
};
@ -47,18 +41,12 @@ class SimilarJobsTab extends React.Component {
this.showNext = this.showNext.bind(this);
this.toggleFilter = this.toggleFilter.bind(this);
this.jobClickUnlisten = this.$rootScope.$on(thEvents.jobClick, (event, job) => {
this.setState({ selectedJob: job, similarJobs: [], isLoading: true }, this.getSimilarJobs);
});
}
componentWillUnmount() {
this.jobClickUnlisten();
this.getSimilarJobs();
}
async getSimilarJobs() {
const { page, selectedJob, similarJobs, selectedSimilarJob } = this.state;
const { repoName } = this.props;
const { page, similarJobs, selectedSimilarJob } = this.state;
const { repoName, selectedJob } = this.props;
const options = {
// get one extra to detect if there are more jobs that can be loaded (hasNextPage)
count: this.pageSize + 1,
@ -146,8 +134,8 @@ class SimilarJobsTab extends React.Component {
const selectedSimilarJobId = selectedSimilarJob ? selectedSimilarJob.id : null;
return (
<div className="similar_jobs w-100">
<div className="left_panel">
<div className="similar-jobs w-100">
<div className="similar-job-list">
<table className="table table-super-condensed table-hover">
<thead>
<tr>
@ -194,7 +182,7 @@ class SimilarJobsTab extends React.Component {
onClick={this.showNext}
>Show previous jobs</button>}
</div>
<div className="right_panel">
<div className="similar-job-detail-panel">
<form className="form form-inline">
<div className="checkbox">
<input
@ -293,8 +281,3 @@ SimilarJobsTab.propTypes = {
$injector: PropTypes.object.isRequired,
repoName: PropTypes.string.isRequired,
};
treeherder.component('similarJobsTab', react2angular(
SimilarJobsTab,
['repoName'],
['$injector']));

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

@ -0,0 +1,240 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import { thEvents } from '../../../js/constants';
import { getAllUrlParams } from '../../../helpers/location';
import { getStatus } from '../../../helpers/job';
import JobDetailsTab from './JobDetailsTab';
import FailureSummaryTab from './failureSummary/FailureSummaryTab';
import PerformanceTab from './PerformanceTab';
import AutoclassifyTab from './autoclassify/AutoclassifyTab';
import AnnotationsTab from './AnnotationsTab';
import SimilarJobsTab from './SimilarJobsTab';
export default class TabsPanel extends React.Component {
static getDerivedStateFromProps(props, state) {
const { perfJobDetail, selectedJob } = props;
const { showAutoclassifyTab } = state;
// This fires every time the props change. But we only want to figure out the new default
// tab when we get a new job. However, the job could change, then later, the perf details fetch
// returns. So we need to check for a change in the size of the perfJobDetail too.
if (state.jobId !== selectedJob.id || state.perfJobDetailSize !== perfJobDetail.length) {
const tabIndex = TabsPanel.getDefaultTabIndex(
getStatus(selectedJob),
!!perfJobDetail.length, showAutoclassifyTab
);
return {
tabIndex,
jobId: selectedJob.id,
perfJobDetailSize: perfJobDetail.length,
};
}
return {};
}
static getTabNames(showPerf, showAutoclassify) {
return [
'details', 'failure', 'autoclassify', 'annotations', 'similar', 'perf'
].filter(name => (
!((name === 'autoclassify' && !showAutoclassify) || (name === 'perf' && !showPerf))
));
}
static getDefaultTabIndex(status, showPerf, showAutoclassify) {
let idx = 0;
const tabNames = TabsPanel.getTabNames(showPerf, showAutoclassify);
const tabIndexes = tabNames.reduce((acc, name) => ({ ...acc, [name]: idx++ }), {});
let tabIndex = showPerf ? tabIndexes.perf : tabIndexes.details;
if (['busted', 'testfailed', 'exception'].includes(status)) {
tabIndex = showAutoclassify ? tabIndexes.autoclassify : tabIndexes.failure;
}
return tabIndex;
}
constructor(props) {
super(props);
const { $injector } = this.props;
this.$rootScope = $injector.get('$rootScope');
this.state = {
showAutoclassifyTab: getAllUrlParams().has('autoclassify'),
tabIndex: 0,
perfJobDetailSize: 0,
jobId: null,
};
}
componentDidMount() {
this.selectNextTabUnlisten = this.$rootScope.$on(thEvents.selectNextTab, () => {
const { tabIndex, showAutoclassifyTab } = this.state;
const { perfJobDetail } = this.props;
const nextIndex = tabIndex + 1;
const tabCount = TabsPanel.getTabNames(!!perfJobDetail.length, showAutoclassifyTab).length;
this.setState({ tabIndex: nextIndex < tabCount ? nextIndex : 0 });
});
this.setTabIndex = this.setTabIndex.bind(this);
}
componentWillUnmount() {
this.selectNextTabUnlisten();
}
setTabIndex(tabIndex) {
this.setState({ tabIndex });
}
render() {
const {
jobDetails, jobLogUrls, logParseStatus, suggestions, errors, pinJob, user, bugs,
bugSuggestionsLoading, selectedJob, perfJobDetail, repoName, jobRevision,
classifications, togglePinBoardVisibility, isPinBoardVisible, pinnedJobs, addBug,
classificationTypes, logViewerFullUrl, reftestUrl, $injector,
} = this.props;
const { showAutoclassifyTab, tabIndex } = this.state;
const countPinnedJobs = Object.keys(pinnedJobs).length;
return (
<div id="tabs-panel">
<Tabs
selectedTabClassName="selected-tab"
selectedIndex={tabIndex}
onSelect={this.setTabIndex}
>
<TabList className="tab-headers">
<span className="tab-header-tabs">
<Tab>Job Details</Tab>
<Tab>Failure Summary</Tab>
{showAutoclassifyTab && <Tab>Failure Classification</Tab>}
<Tab>Annotations</Tab>
<Tab>Similar Jobs</Tab>
{!!perfJobDetail.length && <Tab>Performance</Tab>}
</span>
<span id="tab-header-buttons" className="details-panel-controls pull-right">
<span
id="pinboard-btn"
className="btn pinboard-btn-text"
onClick={togglePinBoardVisibility}
title={isPinBoardVisible ? 'Close the pinboard' : 'Open the pinboard'}
>PinBoard
{!!countPinnedJobs && <div
title={`You have ${countPinnedJobs} job${countPinnedJobs > 1 ? 's' : ''} pinned`}
className={`pin-count-group ${countPinnedJobs > 99 ? 'pin-count-group-3-digit' : ''}`}
>
<div
className={`pin-count-text ${countPinnedJobs > 99 ? 'pin-count-group-3-digit' : ''}`}
>{countPinnedJobs}</div>
</div>}
<span
className={`fa ${isPinBoardVisible ? 'fa-angle-down' : 'fa-angle-up'}`}
/>
</span>
<span
onClick={() => this.$rootScope.$emit(thEvents.clearSelectedJob)}
className="btn details-panel-close-btn"
><span className="fa fa-times" /></span>
</span>
</TabList>
<TabPanel>
<JobDetailsTab jobDetails={jobDetails} />
</TabPanel>
<TabPanel>
<FailureSummaryTab
suggestions={suggestions}
selectedJob={selectedJob}
errors={errors}
bugSuggestionsLoading={bugSuggestionsLoading}
jobLogUrls={jobLogUrls}
logParseStatus={logParseStatus}
addBug={addBug}
pinJob={pinJob}
logViewerFullUrl={logViewerFullUrl}
reftestUrl={reftestUrl}
$injector={$injector}
/>
</TabPanel>
{showAutoclassifyTab && <TabPanel>
<AutoclassifyTab
job={selectedJob}
hasLogs={!!jobLogUrls.length}
logsParsed={logParseStatus !== 'pending'}
logParseStatus={logParseStatus}
addBug={addBug}
pinJob={pinJob}
pinnedJobs={pinnedJobs}
user={user}
$injector={$injector}
/>
</TabPanel>}
<TabPanel>
<AnnotationsTab
classificationTypes={classificationTypes}
classifications={classifications}
selectedJob={selectedJob}
bugs={bugs}
$injector={$injector}
/>
</TabPanel>
<TabPanel>
<SimilarJobsTab
selectedJob={selectedJob}
repoName={repoName}
$injector={$injector}
/>
</TabPanel>
{!!perfJobDetail.length && <TabPanel>
<PerformanceTab
repoName={repoName}
perfJobDetail={perfJobDetail}
revision={jobRevision}
/>
</TabPanel>}
</Tabs>
</div>
);
}
}
TabsPanel.propTypes = {
classificationTypes: PropTypes.object.isRequired,
$injector: PropTypes.object.isRequired,
jobDetails: PropTypes.array.isRequired,
repoName: PropTypes.string.isRequired,
classifications: PropTypes.array.isRequired,
togglePinBoardVisibility: PropTypes.func.isRequired,
isPinBoardVisible: PropTypes.bool.isRequired,
pinnedJobs: PropTypes.object.isRequired,
bugs: PropTypes.array.isRequired,
addBug: PropTypes.func.isRequired,
pinJob: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
perfJobDetail: PropTypes.array,
suggestions: PropTypes.array,
selectedJob: PropTypes.object,
jobRevision: PropTypes.string,
errors: PropTypes.array,
bugSuggestionsLoading: PropTypes.bool,
jobLogUrls: PropTypes.array,
logParseStatus: PropTypes.string,
logViewerFullUrl: PropTypes.string,
reftestUrl: PropTypes.string,
};
TabsPanel.defaultProps = {
suggestions: [],
selectedJob: null,
errors: [],
bugSuggestionsLoading: false,
jobLogUrls: [],
logParseStatus: 'pending',
perfJobDetail: [],
jobRevision: null,
logViewerFullUrl: null,
reftestUrl: null,
};

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

@ -1,21 +1,20 @@
import $ from 'jquery';
import PropTypes from 'prop-types';
import React from 'react';
import { react2angular } from 'react2angular/index.es2015';
import ErrorLineData from './ErrorLineModel';
import AutoclassifyToolbar from './AutoclassifyToolbar';
import ErrorLine from './ErrorLine';
import { getLogViewerUrl, getApiUrl, getProjectJobUrl } from '../../../../helpers/url';
import { thEvents } from '../../../../js/constants';
import treeherder from '../../../../js/treeherder';
import { getLogViewerUrl, getApiUrl, getProjectJobUrl } from '../../../../helpers/url';
import TextLogErrorsModel from '../../../../models/textLogErrors';
class AutoclassifyTab extends React.Component {
import AutoclassifyToolbar from './AutoclassifyToolbar';
import ErrorLine from './ErrorLine';
import ErrorLineData from './ErrorLineModel';
export default class AutoclassifyTab extends React.Component {
static getDerivedStateFromProps(nextProps) {
const { user } = nextProps;
return { canClassify: user.loggedin && user.is_staff };
return { canClassify: user.isLoggedIn && user.isStaff };
}
constructor(props) {
@ -23,9 +22,8 @@ class AutoclassifyTab extends React.Component {
const { $injector } = this.props;
this.$rootScope = $injector.get('$rootScope');
this.thNotify = $injector.get('thNotify');
this.thPinboard = $injector.get('thPinboard');
this.$rootScope = $injector.get('$rootScope');
this.state = {
loadStatus: 'loading',
@ -41,7 +39,7 @@ class AutoclassifyTab extends React.Component {
}
async componentDidMount() {
this.$rootScope.$on(thEvents.jobClick, () => {
this.jobClickUnlisten = this.$rootScope.$on(thEvents.jobClick, () => {
this.setState({
loadStatus: 'loading',
errorLines: [],
@ -49,16 +47,13 @@ class AutoclassifyTab extends React.Component {
editableLineIds: new Set(),
inputByLine: new Map(),
autoclassifyStatusOnLoad: null,
canClassify: false,
});
});
this.$rootScope.$on(
thEvents.autoclassifyChangeSelection,
this.autoclassifyChangeSelectionUnlisten = this.$rootScope.$on(thEvents.autoclassifyChangeSelection,
(ev, direction, clear) => this.onChangeSelection(direction, clear));
this.$rootScope.$on(
thEvents.autoclassifySaveAll,
this.autoclassifySaveAllUnlisten = this.$rootScope.$on(thEvents.autoclassifySaveAll,
() => {
const pendingLines = Array.from(this.state.inputByLine.values());
if (pendingLines.every(line => this.canSave(line.id))) {
@ -69,8 +64,7 @@ class AutoclassifyTab extends React.Component {
}
});
this.$rootScope.$on(
thEvents.autoclassifySave,
this.autoclassifySaveUnlisten = this.$rootScope.$on(thEvents.autoclassifySave,
() => {
const { selectedLineIds, canClassify } = this.state;
@ -83,12 +77,10 @@ class AutoclassifyTab extends React.Component {
}
);
this.$rootScope.$on(
thEvents.autoclassifyToggleEdit,
this.autoclassifyToggleEditUnlisten = this.$rootScope.$on(thEvents.autoclassifyToggleEdit,
() => this.onToggleEditable());
this.$rootScope.$on(
thEvents.autoclassifyOpenLogViewer,
this.autoclassifyOpenLogViewerUnlisten = this.$rootScope.$on(thEvents.autoclassifyOpenLogViewer,
() => this.onOpenLogViewer());
// TODO: Once we're not using ng-react any longer and
@ -123,6 +115,15 @@ class AutoclassifyTab extends React.Component {
}
}
componentWillUnmount() {
this.jobClickUnlisten();
this.autoclassifyChangeSelectionUnlisten();
this.autoclassifySaveAllUnlisten();
this.autoclassifySaveUnlisten();
this.autoclassifyToggleEditUnlisten();
this.autoclassifyOpenLogViewerUnlisten();
}
/**
* Save all pending lines
*/
@ -155,11 +156,11 @@ class AutoclassifyTab extends React.Component {
}
/**
* Pin selected job to the pinboard
* Pin selected job to the pinBoard
*/
onPin() {
//TODO: consider whether this should add bugs or mark all lines as ignored
this.thPinboard.pinJob(this.props.job);
this.props.pinJob(this.props.job);
}
onToggleEditable() {
@ -458,7 +459,7 @@ class AutoclassifyTab extends React.Component {
}
render() {
const { job, autoclassifyStatus, user, $injector } = this.props;
const { job, autoclassifyStatus, user, $injector, addBug, pinnedJobs } = this.props;
const {
errorLines,
loadStatus,
@ -506,6 +507,8 @@ class AutoclassifyTab extends React.Component {
errorLine={errorLine}
prevErrorLine={errorLines[idx - 1]}
canClassify={canClassify}
addBug={addBug}
pinnedJobs={pinnedJobs}
$injector={$injector}
isSelected={selectedLineIds.has(errorLine.id)}
isEditable={editableLineIds.has(errorLine.id)}
@ -523,22 +526,19 @@ class AutoclassifyTab extends React.Component {
AutoclassifyTab.propTypes = {
$injector: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
job: PropTypes.object.isRequired,
hasLogs: PropTypes.bool.isRequired,
pinJob: PropTypes.func.isRequired,
addBug: PropTypes.func.isRequired,
pinnedJobs: PropTypes.object.isRequired,
autoclassifyStatus: PropTypes.string,
user: PropTypes.object,
logsParsed: PropTypes.bool,
logParseStatus: PropTypes.string,
};
AutoclassifyTab.defaultProps = {
autoclassifyStatus: 'pending',
user: { is_staff: false, loggedin: false },
logsParsed: false,
logParseStatus: 'pending',
};
treeherder.component('autoclassifyTab', react2angular(
AutoclassifyTab,
['job', 'hasLogs', 'autoclassifyStatus', 'user', 'logsParsed', 'logParseStatus'],
['$injector']));

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

@ -6,10 +6,10 @@ export default class AutoclassifyToolbar extends React.Component {
getButtonTitle(condition, activeTitle, inactiveTitle) {
const { user } = this.props;
if (!user || !user.loggedin) {
if (!user || !user.isLoggedIn) {
return 'Must be logged in';
}
if (!user.is_staff) {
if (!user.isStaff) {
return 'Insufficeint permissions';
}
if (condition) {

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

@ -3,26 +3,24 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormGroup } from 'reactstrap';
import { thEvents } from '../../../../js/constants';
import { stringOverlap, highlightLogLine } from '../../../../helpers/autoclassify';
import { getBugUrl, getLogViewerUrl } from '../../../../helpers/url';
import LineOption from './LineOption';
import LineOptionModel from './LineOptionModel';
import StaticLineOption from './StaticLineOption';
import { getBugUrl, getLogViewerUrl } from '../../../../helpers/url';
import { stringOverlap, highlightLogLine } from '../../../../helpers/autoclassify';
import { thEvents } from '../../../../js/constants';
const GOOD_MATCH_SCORE = 0.75;
const BAD_MATCH_SCORE = 0.25;
export default class ErrorLine extends React.Component {
constructor(props) {
super(props);
const { $injector, errorLine, setEditable } = this.props;
const { errorLine, setEditable, $injector } = this.props;
this.$rootScope = $injector.get('$rootScope');
this.thPinboard = $injector.get('thPinboard');
this.bestOption = null;
let options = [];
@ -55,19 +53,25 @@ export default class ErrorLine extends React.Component {
}
componentDidMount() {
this.$rootScope.$on(thEvents.autoclassifySelectOption,
(ev, key) => this.onEventSelectOption(key));
this.autoclassifySelectOptionUnlisten = this.$rootScope.$on(thEvents.autoclassifySelectOption,
(ev, key) => this.onEventSelectOption(key));
this.$rootScope.$on(thEvents.autoclassifyIgnore,
this.autoclassifyIgnoreUnlisten = this.$rootScope.$on(thEvents.autoclassifyIgnore,
() => this.onEventIgnore());
this.$rootScope.$on(thEvents.autoclassifyToggleExpandOptions,
this.autoclassifyToggleExpandOptionsUnlisten = this.$rootScope.$on(thEvents.autoclassifyToggleExpandOptions,
() => this.onEventToggleExpandOptions());
this.onOptionChange = this.onOptionChange.bind(this);
this.onManualBugNumberChange = this.onManualBugNumberChange.bind(this);
}
componentWillUnmount() {
this.autoclassifySelectOptionUnlisten();
this.autoclassifyIgnoreUnlisten();
this.autoclassifyToggleExpandOptionsUnlisten();
}
/**
* Select the ignore option, and toggle the ignoreAlways setting if it's
* already selected
@ -484,21 +488,11 @@ export default class ErrorLine extends React.Component {
render() {
const {
errorLine,
job,
canClassify,
isSelected,
isEditable,
setEditable,
$injector,
toggleSelect,
errorLine, job, canClassify, isSelected, isEditable, setEditable,
$injector, toggleSelect, pinnedJobs, addBug,
} = this.props;
const {
messageExpanded,
showHidden,
selectedOption,
options,
extraOptions,
messageExpanded, showHidden, selectedOption, options, extraOptions,
} = this.state;
const failureLine = errorLine.data.metadata.failure_line;
@ -598,8 +592,9 @@ export default class ErrorLine extends React.Component {
canClassify={canClassify}
onOptionChange={this.onOptionChange}
ignoreAlways={option.ignoreAlways}
pinnedJobs={pinnedJobs}
addBug={addBug}
$injector={$injector}
pinBoard={this.thPinboard}
/>
</li>))}
</ul>
@ -625,7 +620,8 @@ export default class ErrorLine extends React.Component {
manualBugNumber={option.manualBugNumber}
ignoreAlways={option.ignoreAlways}
$injector={$injector}
pinBoard={this.thPinboard}
pinnedJobs={pinnedJobs}
addBug={addBug}
/>
</li>))}
</ul>}
@ -639,10 +635,11 @@ export default class ErrorLine extends React.Component {
option={selectedOption}
numOptions={options.length}
canClassify={canClassify}
pinBoard={this.thPinboard}
setEditable={setEditable}
ignoreAlways={selectedOption.ignoreAlways}
manualBugNumber={selectedOption.manualBugNumber}
pinnedJobs={pinnedJobs}
addBug={addBug}
/>
</div>}
</div>
@ -661,6 +658,8 @@ ErrorLine.propTypes = {
setEditable: PropTypes.func.isRequired,
canClassify: PropTypes.bool.isRequired,
$injector: PropTypes.object.isRequired,
pinnedJobs: PropTypes.object.isRequired,
addBug: PropTypes.func.isRequired,
errorMatchers: PropTypes.object,
prevErrorLine: PropTypes.object,
};

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

@ -5,10 +5,10 @@ import Select from 'react-select';
import 'react-select/dist/react-select.css';
import Highlighter from 'react-highlight-words';
import { getBugUrl, getLogViewerUrl, getReftestUrl } from '../../../../helpers/url';
import { isReftest } from '../../../../helpers/job';
import { getSearchWords } from '../../../../helpers/display';
import intermittentTemplate from '../../../../partials/main/intermittent.html';
import { getSearchWords } from '../../../../helpers/display';
import { isReftest } from '../../../../helpers/job';
import { getBugUrl, getLogViewerUrl, getReftestUrl } from '../../../../helpers/url';
/**
* Editable option
@ -76,12 +76,13 @@ export default class LineOption extends React.Component {
optionModel,
selectedOption,
canClassify,
pinBoard,
onOptionChange,
onIgnoreAlwaysChange,
ignoreAlways,
manualBugNumber,
onManualBugNumberChange,
pinnedJobs,
addBug,
} = this.props;
const option = optionModel;
@ -105,10 +106,10 @@ export default class LineOption extends React.Component {
className={canClassify ? '' : 'hidden'}
/>}
{!!option.bugNumber && <span className="line-option-text">
{(!canClassify || pinBoard.isPinned(job)) &&
{(!canClassify || job.id in pinnedJobs) &&
<button
className="btn btn-xs btn-light-bordered"
onClick={() => pinBoard.addBug({ id: option.bugNumber }, job)}
onClick={() => addBug({ id: option.bugNumber }, job)}
title="add to list of bugs to associate with all pinned jobs"
><i className="fa fa-thumb-tack" /></button>}
{!!option.bugResolution &&
@ -176,7 +177,6 @@ export default class LineOption extends React.Component {
</Label>
</FormGroup>
{option.type === 'classifiedFailure' && <div className="classification-matchers">
Matched by:
{option.matches && option.matches.map(match => (<span key={match.matcher.id}>
@ -196,9 +196,10 @@ LineOption.propTypes = {
optionModel: PropTypes.object.isRequired,
canClassify: PropTypes.bool.isRequired,
ignoreAlways: PropTypes.bool.isRequired,
pinBoard: PropTypes.object.isRequired,
selectedOption: PropTypes.object.isRequired,
onOptionChange: PropTypes.func.isRequired,
pinnedJobs: PropTypes.object.isRequired,
addBug: PropTypes.func.isRequired,
onIgnoreAlwaysChange: PropTypes.func,
onManualBugNumberChange: PropTypes.func,
manualBugNumber: PropTypes.number,

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

@ -2,23 +2,16 @@ import React from 'react';
import PropTypes from 'prop-types';
import Highlighter from 'react-highlight-words';
import { getBugUrl } from '../../../../helpers/url';
import { getSearchWords } from '../../../../helpers/display';
import { getBugUrl } from '../../../../helpers/url';
/**
* Non-editable best option
*/
export default function StaticLineOption(props) {
const {
job,
canClassify,
errorLine,
option,
numOptions,
setEditable,
ignoreAlways,
manualBugNumber,
pinBoard,
job, canClassify, errorLine, option, numOptions, setEditable, ignoreAlways,
manualBugNumber, pinnedJobs, addBug,
} = props;
const optionCount = numOptions - 1;
@ -33,10 +26,10 @@ export default function StaticLineOption(props) {
</div>
{!!option.bugNumber && <span className="line-option-text">
{!canClassify || pinBoard.isPinned(job) &&
{(!canClassify || job.id in pinnedJobs) &&
<button
className="btn btn-xs btn-light-bordered"
onClick={() => pinBoard.addBug({ id: option.bugNumber }, job)}
onClick={() => addBug({ id: option.bugNumber }, job)}
title="add to list of bugs to associate with all pinned jobs"
><i className="fa fa-thumb-tack" /></button>}
{!!option.bugResolution &&
@ -86,11 +79,12 @@ StaticLineOption.propTypes = {
job: PropTypes.object.isRequired,
errorLine: PropTypes.object.isRequired,
option: PropTypes.object.isRequired,
pinBoard: PropTypes.object.isRequired,
numOptions: PropTypes.number.isRequired,
ignoreAlways: PropTypes.bool.isRequired,
canClassify: PropTypes.bool.isRequired,
setEditable: PropTypes.func.isRequired,
pinnedJobs: PropTypes.object.isRequired,
addBug: PropTypes.func.isRequired,
manualBugNumber: PropTypes.number,
};

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

@ -2,13 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import Highlighter from 'react-highlight-words';
import { getBugUrl } from '../../../../helpers/url';
import { getSearchWords } from '../../../../helpers/display';
import { getBugUrl } from '../../../../helpers/url';
export default function BugListItem(props) {
const {
bug, suggestion,
bugClassName, title, $timeout, pinboardService, selectedJob,
bug, suggestion, bugClassName, title, selectedJob, addBug,
} = props;
const bugUrl = getBugUrl(bug.id);
@ -16,7 +16,7 @@ export default function BugListItem(props) {
<li>
<button
className="btn btn-xs btn-light-bordered"
onClick={() => $timeout(() => pinboardService.addBug(bug, selectedJob))}
onClick={() => addBug(bug, selectedJob)}
title="add to list of bugs to associate with all pinned jobs"
>
<i className="fa fa-thumb-tack" />
@ -43,8 +43,7 @@ export default function BugListItem(props) {
BugListItem.propTypes = {
bug: PropTypes.object.isRequired,
suggestion: PropTypes.object.isRequired,
$timeout: PropTypes.func.isRequired,
pinboardService: PropTypes.object.isRequired,
addBug: PropTypes.func.isRequired,
selectedJob: PropTypes.object.isRequired,
bugClassName: PropTypes.string,
title: PropTypes.string,

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

@ -10,7 +10,7 @@ export default function ErrorsList(props) {
title="Open in Log Viewer"
target="_blank"
rel="noopener"
href={error.lvURL}
href={error.logViewerUrl}
><span className="ml-1">View log</span></a>
</li>
));

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

@ -1,106 +1,153 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import treeherder from '../../../../js/treeherder';
import intermittentTemplate from '../../../../partials/main/intermittent.html';
import { thEvents } from '../../../../js/constants';
import { isReftest } from '../../../../helpers/job';
import { getBugUrl } from '../../../../helpers/url';
import ErrorsList from './ErrorsList';
import SuggestionsListItem from './SuggestionsListItem';
import ListItem from './ListItem';
import SuggestionsListItem from './SuggestionsListItem';
class FailureSummaryTab extends React.Component {
export default class FailureSummaryTab extends React.Component {
constructor(props) {
super(props);
const { $injector } = this.props;
this.$timeout = $injector.get('$timeout');
this.thPinboard = $injector.get('thPinboard');
this.$uibModal = $injector.get('$uibModal');
this.$rootScope = $injector.get('$rootScope');
}
fileBug(suggestion) {
const { suggestions, jobLogUrls, logViewerFullUrl, selectedJob, reftestUrl, addBug, pinJob } = this.props;
const summary = suggestion.search;
const crashRegex = /application crashed \[@ (.+)\]$/g;
const crash = summary.match(crashRegex);
const crashSignatures = crash ? [crash[0].split('application crashed ')[1]] : [];
const allFailures = suggestions.map(sugg => (sugg.search.split(' | ')));
const modalInstance = this.$uibModal.open({
template: intermittentTemplate,
controller: 'BugFilerCtrl',
size: 'lg',
openedClass: 'filer-open',
resolve: {
summary: () => (summary),
search_terms: () => (suggestion.search_terms),
fullLog: () => (jobLogUrls[0].url),
parsedLog: () => (logViewerFullUrl),
reftest: () => (isReftest(selectedJob) ? reftestUrl : ''),
selectedJob: () => (selectedJob),
allFailures: () => (allFailures),
crashSignatures: () => (crashSignatures),
successCallback: () => (data) => {
// Auto-classify this failure now that the bug has been filed
// and we have a bug number
addBug({ id: data.success });
this.$rootScope.$evalAsync(
this.$rootScope.$emit(
thEvents.saveClassification));
// Open the newly filed bug in a new tab or window for further editing
window.open(getBugUrl(data.success));
}
}
});
pinJob(selectedJob);
modalInstance.opened.then(function () {
window.setTimeout(() => modalInstance.initiate(), 0);
});
}
render() {
const {
fileBug, jobLogUrls, logParseStatus, suggestions, errors,
bugSuggestionsLoading, selectedJob
jobLogUrls, logParseStatus, suggestions, errors,
bugSuggestionsLoading, selectedJob, addBug
} = this.props;
const logs = jobLogUrls;
const jobLogsAllParsed = logs.every(jlu => (jlu.parse_status !== 'pending'));
return (
<ul className="list-unstyled failure-summary-list" ref={this.fsMount}>
{suggestions.map((suggestion, index) =>
(<SuggestionsListItem
key={index} // eslint-disable-line react/no-array-index-key
index={index}
suggestion={suggestion}
fileBug={fileBug}
pinboardService={this.thPinboard}
selectedJob={selectedJob}
$timeout={this.$timeout}
/>))}
<div className="w-100 h-100">
<ul className="list-unstyled failure-summary-list" ref={this.fsMount}>
{suggestions.map((suggestion, index) =>
(<SuggestionsListItem
key={index} // eslint-disable-line react/no-array-index-key
index={index}
suggestion={suggestion}
toggleBugFiler={() => this.fileBug(suggestion)}
selectedJob={selectedJob}
addBug={addBug}
/>))}
{!!errors.length &&
<ErrorsList errors={errors} />}
{!!errors.length &&
<ErrorsList errors={errors} />}
{!bugSuggestionsLoading && jobLogsAllParsed &&
!logs.length && !suggestions.length && !errors.length &&
<ListItem text="Failure summary is empty" />}
{!bugSuggestionsLoading && jobLogsAllParsed &&
!logs.length && !suggestions.length && !errors.length &&
<ListItem text="Failure summary is empty" />}
{!bugSuggestionsLoading && jobLogsAllParsed && !!logs.length &&
logParseStatus === 'success' &&
<li>
<p className="failure-summary-line-empty mb-0">Log parsing complete. Generating bug suggestions.<br />
<span>The content of this panel will refresh in 5 seconds.</span></p>
</li>}
{!bugSuggestionsLoading && jobLogsAllParsed && !!logs.length &&
logParseStatus === 'success' &&
<li>
<p className="failure-summary-line-empty mb-0">Log parsing complete. Generating bug suggestions.<br />
<span>The content of this panel will refresh in 5 seconds.</span></p>
</li>}
{!bugSuggestionsLoading && !jobLogsAllParsed &&
logs.map(jobLog =>
(<li key={jobLog.id}>
<p className="failure-summary-line-empty mb-0">Log parsing in progress.<br />
<a
title="Open the raw log in a new window"
target="_blank"
rel="noopener"
href={jobLog.url}
>The raw log</a> is available. This panel will automatically recheck every 5 seconds.</p>
</li>))}
{!bugSuggestionsLoading && !jobLogsAllParsed &&
logs.map(jobLog =>
(<li key={jobLog.id}>
<p className="failure-summary-line-empty mb-0">Log parsing in progress.<br />
<a
title="Open the raw log in a new window"
target="_blank"
rel="noopener"
href={jobLog.url}
>The raw log</a> is available. This panel will automatically recheck every 5 seconds.</p>
</li>))}
{!bugSuggestionsLoading && logParseStatus === 'failed' &&
<ListItem text="Log parsing failed. Unable to generate failure summary." />}
{!bugSuggestionsLoading && logParseStatus === 'failed' &&
<ListItem text="Log parsing failed. Unable to generate failure summary." />}
{!bugSuggestionsLoading && !logs.length &&
<ListItem text="No logs available for this job." />}
{!bugSuggestionsLoading && !logs.length &&
<ListItem text="No logs available for this job." />}
{bugSuggestionsLoading &&
<div className="overlay">
<div>
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
</div>
</div>}
</ul>
{bugSuggestionsLoading &&
<div className="overlay">
<div>
<span className="fa fa-spinner fa-pulse th-spinner-lg" />
</div>
</div>}
</ul>
</div>
);
}
}
FailureSummaryTab.propTypes = {
$injector: PropTypes.object.isRequired,
fileBug: PropTypes.func.isRequired,
addBug: PropTypes.func.isRequired,
pinJob: PropTypes.func.isRequired,
suggestions: PropTypes.array,
selectedJob: PropTypes.object,
errors: PropTypes.array,
bugSuggestionsLoading: PropTypes.bool,
jobLogUrls: PropTypes.array,
logParseStatus: PropTypes.string,
reftestUrl: PropTypes.string,
logViewerFullUrl: PropTypes.string,
};
FailureSummaryTab.defaultProps = {
suggestions: [],
selectedJob: null,
reftestUrl: null,
errors: [],
bugSuggestionsLoading: false,
jobLogUrls: [],
logParseStatus: 'pending',
logViewerFullUrl: null,
};
treeherder.component('failureSummaryTab', react2angular(
FailureSummaryTab,
['fileBug', 'suggestions', 'selectedJob', 'errors', 'bugSuggestionsLoading', 'jobLogUrls', 'logParseStatus'],
['$injector']));

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

@ -1,9 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import BugListItem from './BugListItem';
import { thBugSuggestionLimit } from '../../../../js/constants';
import BugListItem from './BugListItem';
export default class SuggestionsListItem extends React.Component {
constructor(props) {
super(props);
@ -20,16 +21,16 @@ export default class SuggestionsListItem extends React.Component {
render() {
const {
suggestion, selectedJob, $timeout, pinboardService, fileBug, index
suggestion, selectedJob, toggleBugFiler, addBug,
} = this.props;
const { suggestionShowMore } = this.state;
return (
<li>
<div className="job-tabs-content">
<div>
<span
className="btn btn-xs btn-light-bordered link-style"
onClick={() => fileBug(index)}
onClick={() => toggleBugFiler(suggestion)}
title="file a bug for this failure"
>
<i className="fa fa-bug" />
@ -45,9 +46,8 @@ export default class SuggestionsListItem extends React.Component {
key={bug.id}
bug={bug}
selectedJob={selectedJob}
pinboardService={pinboardService}
suggestion={suggestion}
$timeout={$timeout}
addBug={addBug}
/>))}
</ul>}
@ -68,11 +68,10 @@ export default class SuggestionsListItem extends React.Component {
key={bug.id}
bug={bug}
selectedJob={selectedJob}
pinboardService={pinboardService}
suggestion={suggestion}
$timeout={$timeout}
bugClassName={bug.resolution !== "" ? "deleted" : ""}
title={bug.resolution !== "" ? bug.resolution : ""}
bugClassName={bug.resolution !== '' ? 'deleted' : ''}
title={bug.resolution !== '' ? bug.resolution : ''}
addBug={addBug}
/>))}
</ul>}
@ -87,8 +86,6 @@ export default class SuggestionsListItem extends React.Component {
SuggestionsListItem.propTypes = {
suggestion: PropTypes.object.isRequired,
selectedJob: PropTypes.object.isRequired,
$timeout: PropTypes.func.isRequired,
pinboardService: PropTypes.object.isRequired,
fileBug: PropTypes.func.isRequired,
index: PropTypes.number.isRequired,
addBug: PropTypes.func.isRequired,
toggleBugFiler: PropTypes.func.isRequired,
};

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

@ -53,4 +53,4 @@ export const parseHash = qs => (
})
);
export const loggedOutUser = { is_staff: false, username: "", email: "", loggedin: false };
export const loggedOutUser = { isStaff: false, username: "", email: "", isLoggedIn: false };

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

@ -18,7 +18,7 @@ import UserModel from '../../models/user';
treeherder.component("login", {
template: `
<span class="dropdown"
ng-if="$ctrl.user.loggedin">
ng-if="$ctrl.user.isLoggedIn">
<button id="logoutLabel" title="Logged in as: {{$ctrl.user.email}}" role="button"
data-toggle="dropdown"
class="btn btn-view-nav">
@ -37,7 +37,7 @@ treeherder.component("login", {
</span>
<span class="btn nav-login-btn"
ng-if="!$ctrl.user.loggedin"
ng-if="!$ctrl.user.isLoggedIn"
ng-click="$ctrl.login()">Login/Register</span>
`,
bindings: {
@ -110,7 +110,7 @@ treeherder.component("login", {
ctrl.setLoggedIn = function (newUser) {
const userSession = JSON.parse(localStorage.getItem('userSession'));
newUser.loggedin = true;
newUser.isLoggedIn = true;
newUser.fullName = userSession.fullName;
ctrl.user = newUser;

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

@ -242,6 +242,8 @@ export const thJobNavSelectors = {
export const thPinboardCountError = "Max pinboard size of 500 reached.";
export const thPinboardMaxSize = 500;
export const thPerformanceBranches = ["autoland", "mozilla-inbound"];
/**

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

@ -153,12 +153,6 @@ treeherder.controller('BugFilerCtrl', [
}
$scope.modalSummary = "Intermittent " + summaryString;
$scope.toggleFilerSummaryVisibility = function () {
$scope.isFilerSummaryVisible = !$scope.isFilerSummaryVisible;
};
$scope.isFilerSummaryVisible = false;
// Add a product/component pair to suggestedProducts
const addProduct = function (product) {
// Don't allow duplicates to be added to the list

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

@ -1,14 +1,9 @@
import treeherderApp from '../treeherder_app';
import { thFailureResults, thPinboardCountError, thAllResultStates, thEvents } from "../constants";
import { thFailureResults, thAllResultStates, thEvents } from "../constants";
treeherderApp.controller('JobFilterCtrl', [
'$scope', '$rootScope',
'thJobFilters',
'ThResultSetStore', 'thPinboard', 'thNotify',
function JobFilterCtrl(
$scope, $rootScope,
thJobFilters,
ThResultSetStore, thPinboard, thNotify) {
'$scope', '$rootScope', 'thJobFilters',
function JobFilterCtrl($scope, $rootScope, thJobFilters) {
$scope.filterOptions = thAllResultStates;
@ -87,22 +82,6 @@ treeherderApp.controller('JobFilterCtrl', [
func(thJobFilters.classifiedState, 'unclassified');
};
$scope.pinAllShownJobs = function () {
if (!thPinboard.spaceRemaining()) {
thNotify.send(thPinboardCountError, 'danger', { sticky: true });
return;
}
const shownJobs = ThResultSetStore.getAllShownJobs(
thPinboard.spaceRemaining(),
thPinboardCountError
);
thPinboard.pinJobs(shownJobs);
if (!$rootScope.selectedJob) {
$rootScope.selectedJob = shownJobs[0];
}
};
$scope.thJobFilters = thJobFilters;
const updateToggleFilters = function () {

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

@ -7,14 +7,14 @@ import { thTitleSuffixLimit, thDefaultRepo, thJobNavSelectors, thEvents } from "
treeherderApp.controller('MainCtrl', [
'$scope', '$rootScope', '$location', '$timeout', '$q',
'ThRepositoryModel', 'thPinboard', 'thTabs', '$document',
'ThRepositoryModel', '$document',
'thClassificationTypes', '$interval', '$window',
'thJobFilters', 'ThResultSetStore', 'thNotify',
'$http',
'$httpParamSerializer',
function MainController(
$scope, $rootScope, $location, $timeout, $q,
ThRepositoryModel, thPinboard, thTabs, $document,
ThRepositoryModel, $document,
thClassificationTypes, $interval, $window,
thJobFilters, ThResultSetStore, thNotify,
$http,
@ -48,6 +48,9 @@ treeherderApp.controller('MainCtrl', [
$rootScope.revision = $location.search().revision;
thClassificationTypes.load();
// TODO: remove this once we're off of Angular completely.
$rootScope.countPinnedJobs = () => 0;
const checkServerRevision = function () {
return $q(function (resolve, reject) {
$http({
@ -149,17 +152,6 @@ treeherderApp.controller('MainCtrl', [
return title;
};
$rootScope.closeJob = function () {
// Setting the selectedJob to null closes the bottom panel
$rootScope.selectedJob = null;
// Clear the selected job display style
$rootScope.$emit(thEvents.clearSelectedJob);
// Reset selected job to null to initialize nav position
ThResultSetStore.setSelectedJob();
};
$scope.repoModel = ThRepositoryModel;
/**
@ -281,11 +273,9 @@ treeherderApp.controller('MainCtrl', [
const keyShortcuts = [
// Shortcut: select all remaining unverified lines on the current job
['a', () => {
if (thTabs.selectedTab === "autoClassification") {
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifyChangeSelection,
'all_next',
false));
}
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifyChangeSelection,
'all_next',
false));
}],
// Shortcut: pin selected job to pinboard and add a related bug
@ -333,9 +323,7 @@ treeherderApp.controller('MainCtrl', [
// Shortcut: toggle edit mode for selected lines
['e', () => {
if (thTabs.selectedTab === "autoClassification") {
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifyToggleEdit));
}
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifyToggleEdit));
}],
// Shortcut: enter a quick filter
@ -359,9 +347,7 @@ treeherderApp.controller('MainCtrl', [
// Shortcut: ignore selected in the autoclasify panel
['shift+i', () => {
if (thTabs.selectedTab === "autoClassification") {
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifyIgnore));
}
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifyIgnore));
}],
// Shortcut: select next unclassified failure
@ -373,11 +359,9 @@ treeherderApp.controller('MainCtrl', [
// Shortcut: select next unverified log line
[['down', 'shift+down'], (ev) => {
if (thTabs.selectedTab === "autoClassification") {
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifyChangeSelection,
'next',
!ev.shiftKey));
}
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifyChangeSelection,
'next',
!ev.shiftKey));
}],
// Shortcut: select previous unclassified failure
@ -389,20 +373,14 @@ treeherderApp.controller('MainCtrl', [
// Shortcut: select previous unverified log line
[['up', 'shift+up'], (ev) => {
if (thTabs.selectedTab === "autoClassification") {
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifyChangeSelection,
'previous',
!ev.shiftKey));
}
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifyChangeSelection,
'previous',
!ev.shiftKey));
}],
// Shortcut: open the logviewer for the selected job
['l', () => {
if (thTabs.selectedTab === "autoClassification") {
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifyOpenLogViewer));
} else if ($scope.selectedJob) {
$scope.$evalAsync($rootScope.$emit(thEvents.openLogviewer));
}
$scope.$evalAsync($rootScope.$emit(thEvents.openLogviewer));
}],
// Shortcut: Next/prev unclassified failure
@ -420,9 +398,7 @@ treeherderApp.controller('MainCtrl', [
// Shortcut: save all in the autoclasify panel
['s', () => {
if (thTabs.selectedTab === "autoClassification") {
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifySaveAll));
}
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifySaveAll));
}],
// Shortcut: select next job tab
@ -446,17 +422,13 @@ treeherderApp.controller('MainCtrl', [
// Shortcut: toggle more/fewer options in the autoclassify panel
['x', () => {
if (thTabs.selectedTab === "autoClassification") {
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifyToggleExpandOptions));
}
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifyToggleExpandOptions));
}],
// Shortcut: ignore selected in the autoclasify panel
[['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'o'], (ev) => {
if (thTabs.selectedTab === "autoClassification") {
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifySelectOption,
ev.key === "o" ? "manual" : ev.key));
}
$scope.$evalAsync($rootScope.$emit(thEvents.autoclassifySelectOption,
ev.key === "o" ? "manual" : ev.key));
}],
// Shortcut: select previous job
@ -489,7 +461,7 @@ treeherderApp.controller('MainCtrl', [
// Shortcut: escape closes any open panels and clears selected job
['escape', () => {
$scope.$evalAsync($scope.closeJob());
$scope.$evalAsync($rootScope.$emit(thEvents.clearSelectedJob));
$scope.$evalAsync($scope.setOnscreenShortcutsShowing(false));
}],
@ -696,8 +668,6 @@ treeherderApp.controller('MainCtrl', [
$scope.onscreenOverlayShowing = tf;
};
$scope.pinboardCount = thPinboard.count;
$scope.pinnedJobs = thPinboard.pinnedJobs;
$scope.jobFilters = thJobFilters;
$scope.isShowDuplicateJobs = function () {

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

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

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

@ -273,7 +273,7 @@ treeherder.factory('ThResultSetStore', [
}
};
var getAllShownJobs = function (spaceRemaining, errorMessage, pushId) {
var getAllShownJobs = (pushId) => {
var shownJobs = [];
var addIfShown = function (jMap) {
@ -283,10 +283,6 @@ treeherder.factory('ThResultSetStore', [
if (jMap.job_obj.visible) {
shownJobs.push(jMap.job_obj);
}
if (shownJobs.length === spaceRemaining) {
thNotify.send(errorMessage, 'danger');
return true;
}
return false;
};
_.find(getJobMap(), addIfShown);

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

@ -1,176 +0,0 @@
import _ from 'lodash';
import treeherder from '../treeherder';
import { thPinboardCountError, thEvents } from "../constants";
import JobClassificationModel from '../../models/classification';
import BugJobMapModel from '../../models/bugJobMap';
import { formatModelError } from '../../helpers/errorMessage';
treeherder.factory('thPinboard', [
'$rootScope', 'thNotify', 'ThResultSetStore', '$timeout',
function (
$rootScope, thNotify, ThResultSetStore, $timeout) {
const pinnedJobs = {};
const relatedBugs = {};
const saveClassification = function (job) {
const classification = new JobClassificationModel(this);
// classification can be left unset making this a no-op
if (classification.failure_classification_id > 0) {
job.failure_classification_id = classification.failure_classification_id;
// update the unclassified failure count for the page
ThResultSetStore.updateUnclassifiedFailureMap(job);
classification.job_id = job.id;
classification.create()
.then(() => {
$timeout(() => thNotify.send("Classification saved for " + job.platform + " " + job.job_type_name, "success"));
}).catch((response) => {
const message = "Error saving classification for " + job.platform + " " + job.job_type_name;
$timeout(() => thNotify.send(formatModelError(response, message), "danger"));
});
}
};
const saveBugs = function (job) {
Object.values(relatedBugs).forEach(function (bug) {
const bjm = new BugJobMapModel({
bug_id: bug.id,
job_id: job.id,
type: 'annotation'
});
bjm.create()
.then($timeout(() => {
thNotify.send("Bug association saved for " + job.platform + " " + job.job_type_name, "success");
}).catch((response) => {
const message = "Error saving bug association for " + job.platform + " " + job.job_type_name;
$timeout(() => thNotify.send(formatModelError(response, message), "danger"));
}));
});
};
const api = {
toggleJobPin: function (job) {
if (pinnedJobs[job.id]) {
api.unPinJob(job.id);
} else {
api.pinJob(job);
}
},
pinJob: function (job) {
if (api.spaceRemaining() > 0) {
pinnedJobs[job.id] = job;
api.count.numPinnedJobs = Object.keys(pinnedJobs).length;
$rootScope.$emit(thEvents.pulsePinCount);
} else {
thNotify.send(thPinboardCountError, 'danger');
}
},
pinJobs: function (jobsToPin) {
jobsToPin.forEach(api.pinJob);
},
unPinJob: function (id) {
delete pinnedJobs[id];
api.count.numPinnedJobs = Object.keys(pinnedJobs).length;
},
// clear all pinned jobs and related bugs
unPinAll: function () {
for (const jid in pinnedJobs) {
if (pinnedJobs.hasOwnProperty(jid)) { delete pinnedJobs[jid]; }
}
for (const bid in relatedBugs) {
if (relatedBugs.hasOwnProperty(bid)) { delete relatedBugs[bid]; }
}
api.count.numPinnedJobs = Object.keys(pinnedJobs).length;
},
addBug: (bug, job) => {
relatedBugs[bug.id] = bug;
api.count.numRelatedBugs = Object.keys(relatedBugs).length;
if (job) {
api.pinJob(job);
}
},
removeBug: function (id) {
delete relatedBugs[id];
api.count.numRelatedBugs = Object.keys(relatedBugs).length;
},
// open form to create a new note. default to intermittent
createNewClassification: function () {
return new JobClassificationModel({
text: "",
who: null,
failure_classification_id: 4
});
},
// save the classification and related bugs to all pinned jobs
save: function (classification) {
const pinnedJobsClone = {};
let jid;
for (jid in pinnedJobs) {
if (pinnedJobs.hasOwnProperty(jid)) {
pinnedJobsClone[jid] = pinnedJobs[jid];
}
}
_.each(pinnedJobs, saveClassification.bind(classification));
$rootScope.$emit(thEvents.jobsClassified, { jobs: pinnedJobsClone });
_.each(pinnedJobs, saveBugs);
$rootScope.$emit(thEvents.bugsAssociated, { jobs: pinnedJobsClone });
api.unPinAll();
},
// save the classification only on all pinned jobs
saveClassificationOnly: function (classification) {
_.each(pinnedJobs, saveClassification.bind(classification));
$rootScope.$emit(thEvents.jobsClassified, { jobs: pinnedJobs });
},
// save bug associations only on all pinned jobs
saveBugsOnly: function () {
_.each(pinnedJobs, saveBugs);
$rootScope.$emit(thEvents.bugsAssociated, { jobs: pinnedJobs });
},
hasPinnedJobs: function () {
return !_.isEmpty(pinnedJobs);
},
hasRelatedBugs: function () {
return !_.isEmpty(relatedBugs);
},
spaceRemaining: function () {
return api.maxNumPinned - api.count.numPinnedJobs;
},
isPinned: function (job) {
return pinnedJobs.hasOwnProperty(job.id);
},
pinnedJobs: pinnedJobs,
relatedBugs: relatedBugs,
count: {
numPinnedJobs: 0,
numRelatedBugs: 0
},
// not sure what this should be, but we need some limit, I think.
maxNumPinned: 500
};
return api;
}]);

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

@ -2,22 +2,13 @@ import angular from 'angular';
import hcMarked from 'angular-marked';
import ngRoute from 'angular-route';
import uiBootstrap from 'angular1-ui-bootstrap4';
import mcResizer from '../vendor/resizer';
import treeherderModule from './treeherder';
import AnnotationsTemplate from '../plugins/annotations/main.html';
import AutoClassificationTemplate from '../plugins/auto_classification/main.html';
import FailureSummaryTemplate from '../plugins/failure_summary/main.html';
import JobDetailsTemplate from '../plugins/job_details/main.html';
import PerfDetailsTemplate from '../plugins/perf_details/main.html';
import pluginPanelTemplate from '../plugins/pluginpanel.html';
import SimilarJobsTemplate from '../plugins/similar_jobs/main.html';
import thActiveFiltersBarTemplate from '../partials/main/thActiveFiltersBar.html';
import thFilterChickletsTemplate from '../partials/main/thFilterChicklets.html';
import thGlobalTopNavPanelTemplate from '../partials/main/thGlobalTopNavPanel.html';
import thHelpMenuTemplate from '../partials/main/thHelpMenu.html';
import thInfraMenuTemplate from '../partials/main/thInfraMenu.html';
import thPinboardPanelTemplate from '../partials/main/thPinboardPanel.html';
import thShortcutTableTemplate from '../partials/main/thShortcutTable.html';
import thTreeherderUpdateBarTemplate from '../partials/main/thTreeherderUpdateBar.html';
import thWatchedRepoNavPanelTemplate from '../partials/main/thWatchedRepoNavPanel.html';
@ -26,8 +17,6 @@ const treeherderApp = angular.module('treeherder.app', [
treeherderModule.name,
uiBootstrap,
ngRoute,
// Remove when `ui/plugins/pluginpanel.html` converted to React.
mcResizer,
// Remove when `ui/partials/main/tcjobactions.html` converted to React.
hcMarked,
]);
@ -80,17 +69,9 @@ treeherderApp.config(['$compileProvider', '$locationProvider', '$routeProvider',
$templateCache.put('partials/main/thGlobalTopNavPanel.html', thGlobalTopNavPanelTemplate);
$templateCache.put('partials/main/thHelpMenu.html', thHelpMenuTemplate);
$templateCache.put('partials/main/thInfraMenu.html', thInfraMenuTemplate);
$templateCache.put('partials/main/thPinboardPanel.html', thPinboardPanelTemplate);
$templateCache.put('partials/main/thShortcutTable.html', thShortcutTableTemplate);
$templateCache.put('partials/main/thTreeherderUpdateBar.html', thTreeherderUpdateBarTemplate);
$templateCache.put('partials/main/thWatchedRepoNavPanel.html', thWatchedRepoNavPanelTemplate);
$templateCache.put('plugins/annotations/main.html', AnnotationsTemplate);
$templateCache.put('plugins/auto_classification/main.html', AutoClassificationTemplate);
$templateCache.put('plugins/failure_summary/main.html', FailureSummaryTemplate);
$templateCache.put('plugins/job_details/main.html', JobDetailsTemplate);
$templateCache.put('plugins/perf_details/main.html', PerfDetailsTemplate);
$templateCache.put('plugins/pluginpanel.html', pluginPanelTemplate);
$templateCache.put('plugins/similar_jobs/main.html', SimilarJobsTemplate);
}]);
export default treeherderApp;

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

@ -5,6 +5,7 @@ const uri = getApiUrl('/user/');
export default class UserModel {
constructor(data) {
Object.assign(this, data);
this.isStaff = data.is_staff;
}
static get() {

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

@ -27,10 +27,10 @@
</div>
</div>
<div class="modal-footer">
<button ng-if="user.loggedin" class="btn btn-primary-soft" ng-click="triggerAction()" ng-attr-title="{{user.loggedin ? 'Trigger this action' : 'Not logged in'}}" ng-disabled="triggering">
<button ng-if="user.isLoggedIn" class="btn btn-primary-soft" ng-click="triggerAction()" ng-attr-title="{{user.isLoggedIn ? 'Trigger this action' : 'Not logged in'}}" ng-disabled="triggering">
<span class="fa fa-check-square-o" aria-hidden="true"></span>
<span ng-if="triggering">Triggering</span>
<span ng-if="!triggering">Trigger</span>
</button>
<p ng-if="!user.loggedin" class="help-block">Custom actions require login</p>
<p ng-if="!user.isLoggedIn" class="help-block">Custom actions require login</p>
</div>

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

@ -153,7 +153,7 @@
<li title="Pin all jobs that pass the global filters"
class="dropdown-item"
ng-click="pinAllShownJobs()">Pin all showing</li>
ng-click="pinJobs()">Pin all showing</li>
<li title="Show only superseded jobs"
class="dropdown-item"
ng-click="thJobFilters.setOnlySuperseded()">Superseded only</li>

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

@ -1,112 +0,0 @@
<!-- Pinboard -->
<div id="pinned-job-list">
<div class="content">
<span class="pinboard-preload-txt"
ng-hide="hasPinnedJobs()">
press spacebar to pin a selected job</span>
<span ng-repeat="job in pinnedJobs">
<th-pinned-job></th-pinned-job>
</span>
</div>
</div>
<!-- Related bugs -->
<div id="pinboard-related-bugs">
<div class="content">
<a ng-click="toggleEnterBugNumber(!enteringBugNumber); allowKeys()"
class="pointable"
title="Add a related bug">
<i class="fa fa-plus-square add-related-bugs-icon"></i>
</a>
<span class="pinboard-preload-txt pinboard-related-bug-preload-txt"
ng-if="!hasRelatedBugs()"
ng-click="toggleEnterBugNumber(!enteringBugNumber); allowKeys()">
click to add a related bug</span>
<form ng-submit="saveEnteredBugNumber()"
ng-show="enteringBugNumber"
class="add-related-bugs-form">
<input id="related-bug-input"
data-bug-input
class="add-related-bugs-input"
ng-model="$parent.newEnteredBugNumber"
placeholder="enter bug number"
ng-keypress="ctrlEnterSaves($event)"
focus-me="focusInput">
</form>
<span ng-repeat="bug in relatedBugs">
<th-related-bug-queued></th-related-bug-queued>
</span>
</div>
</div>
<!-- Classification dropdown -->
<div id="pinboard-classification">
<div class="pinboard-label">classification</div>
<div id="pinboard-classification-content" class="content">
<form ng-submit="completeClassification()" class="form">
<select id="pinboard-classification-select"
ng-model="classification.failure_classification_id"
ng-options="item.id as item.name for item in classificationTypes.classificationOptions">
</select>
<!-- Classification comment -->
<div class="classification-comment-container">
<input id="classification-comment"
type="text"
class="form-control add-classification-input"
ng-model="classification.text"
ng-click="allowKeys()"
ng-paste="pasteSHA($event)"
placeholder="click to add comment"
blur-this></input>
<div ng-if="classification.failure_classification_id === 2">
<select id="recent-choice"
ng-model="classification.recentChoice"
ng-change="classification.text=classification.recentChoice">
<option value="0" selected disabled>Choose a recent commit</option>
<option ng-repeat="tip in revisionList | limitTo:20"
title="{{tip.title}}"
value="{{tip.revision}}">
{{tip.revision | limitTo: 12}} {{tip.author}}
</option>
</select>
</div>
</div>
</form>
</div>
</div>
<!-- Save UI -->
<div id="pinboard-controls" class="btn-group-vertical"
title="{{!hasPinnedJobs() ? 'No pinned jobs' : ''}}">
<div class="btn-group save-btn-group dropdown">
<button class="btn btn-light-bordered btn-xs save-btn"
title="{{ saveUITitle('classification') }}"
ng-click="save()"
ng-disabled="!user.loggedin || !canSaveClassifications()">save
</button>
<button class="btn btn-light-bordered btn-xs dropdown-toggle save-btn-dropdown"
title="{{ !hasPinnedJobs() && !pinboardIsDirty() ? 'No pinned jobs' :
'Additional pinboard functions' }}"
ng-disabled="!hasPinnedJobs() && !pinboardIsDirty()"
type="button"
data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu save-btn-dropdown-menu">
<li class="{{ !user.loggedin || !canSaveClassifications() ? 'disabled' : '' }}"
title="{{ saveUITitle('classification') }}">
<a class="dropdown-item" ng-click="!user.loggedin || !canSaveClassifications() || saveClassificationOnly()">Save classification only</a></li>
<li class="{{ !user.loggedin || !hasRelatedBugs() ? 'disabled' : '' }}"
title="{{ saveUITitle('bug') }}">
<a class="dropdown-item" ng-click="!user.loggedin || !canSaveClassifications() || !hasRelatedBugs() || saveBugsOnly()">Save bugs only</a></li>
<li class="{{ !user.loggedin ? 'disabled' : '' }}"
title="{{ !user.loggedin ? 'Not logged in' : 'Repeat the pinned jobs'}}">
<a class="dropdown-item" ng-click="!user.loggedin || retriggerAllPinnedJobs()">Retrigger all</a></li>
<li class="{{ canCancelAllPinnedJobs() ? '' : 'disabled' }}"
title="{{ cancelAllPinnedJobsTitle() }}">
<a class="dropdown-item" ng-click="canCancelAllPinnedJobs() && cancelAllPinnedJobs()">Cancel all</a>
</li>
<li><a class="dropdown-item" ng-click="unPinAll()">Clear all</a></li>
</ul>
</div>
</div>

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

@ -1,11 +0,0 @@
<span class="btn-group">
<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)"
data-job-id="{{ job.job_id }}">{{ job.job_type_symbol }}</span>
<span class="btn btn-ltgray pinned-job-close-btn"
ng-class="{'btn-lg selected-job': (selectedJob==job), 'btn-xs': (selectedJob!=job)}"
ng-click="unPinJob(job.id)"
title="un-pin this job"><i class="fa fa-times"></i></span>
</span>

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

@ -1,9 +0,0 @@
<span class="btn-group pinboard-related-bugs-btn">
<a class="btn btn-xs related-bugs-link"
title="{{::bug.summary}}"
href="{{:: getBugUrl(bug.id) }}"
target="_blank" rel="noopener"><em>{{::bug.id}}</em></a>
<span class="btn btn-ltgray btn-xs pinned-job-close-btn"
ng-click="removeBug(bug.id)"
title="remove this bug"><i class="fa fa-times"></i></span>
</span>

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

@ -1,5 +1,5 @@
<div class="container-fluid alerts-container">
<div class="alert alert-warning" ng-show="!user.is_staff" role="alert">
<div class="alert alert-warning" ng-show="!user.isStaff" role="alert">
You must be logged into perfherder/treeherder and be a sheriff to make changes
</div>
<form class="form-inline">
@ -40,7 +40,7 @@
<div class="card alert-summary" ng-repeat="alertSummary in alertSummaries" ng-if="alertSummary.anyVisible">
<div class="card-header alert-summary-heading">
<div class="alert-summary-header-element">
<input type="checkbox" ng-disabled="!user.is_staff" ng-model="alertSummary.allSelected" ng-change="selectNoneOrSelectAll(alertSummary)"/><!-- select 'em all checkbox -->
<input type="checkbox" ng-disabled="!user.isStaff" ng-model="alertSummary.allSelected" ng-change="selectNoneOrSelectAll(alertSummary)"/><!-- select 'em all checkbox -->
</div>
<div class="alert-summary-title">
<a class="anchor" href="#/alerts?id={{alertSummary.id}}" ng-class="{'alert-summary-title-invalid': alertSummary.status==4}">
@ -82,30 +82,30 @@
<li role="menuitem" ng-show="!alertSummary.bug_number">
<a ng-click="fileBug(alertSummary)" class="dropdown-item">File bug</a>
</li>
<li role="menuitem" ng-show="!alertSummary.bug_number && user.is_staff">
<li role="menuitem" ng-show="!alertSummary.bug_number && user.isStaff">
<a ng-click="linkToBug(alertSummary)" class="dropdown-item">Link to bug</a>
</li>
<li role="menuitem" ng-show="alertSummary.bug_number && user.is_staff">
<li role="menuitem" ng-show="alertSummary.bug_number && user.isStaff">
<a ng-click="unlinkBug(alertSummary)" class="dropdown-item">Unlink from bug</a>
</li>
<li role="menuitem" ng-show="user.is_staff">
<li role="menuitem" ng-show="user.isStaff">
<a ng-click="editAlertSummaryNotes(alertSummary)" class="dropdown-item">
<span ng-if="!alertSummary.notes">Add notes</span>
<span ng-if="alertSummary.notes">Edit notes</span>
</a>
</li>
<li role="menuitem" ng-show="user.is_staff" ng-if="alertSummary.isResolved()">
<li role="menuitem" ng-show="user.isStaff" ng-if="alertSummary.isResolved()">
<a ng-click="alertSummary.markInvestigating()" class="dropdown-item">Re-open</a>
</li>
<li role="menuitem" ng-show="user.is_staff"
<li role="menuitem" ng-show="user.isStaff"
ng-if="alertSummary.isInvestigating() || (alertSummary.isResolved() && !alertSummary.isWontfix())">
<a ng-click="alertSummary.markWontfix()" class="dropdown-item">Mark as "won't fix"</a>
</li>
<li role="menuitem" ng-show="user.is_staff"
<li role="menuitem" ng-show="user.isStaff"
ng-if="alertSummary.isInvestigating() || (alertSummary.isResolved() && !alertSummary.isBackedout())">
<a ng-click="alertSummary.markBackedout()" class="dropdown-item">Mark as backed out</a>
</li>
<li role="menuitem" ng-show="user.is_staff"
<li role="menuitem" ng-show="user.isStaff"
ng-if="alertSummary.isInvestigating() || (alertSummary.isResolved() && !alertSummary.isFixed())">
<a ng-click="alertSummary.markFixed()" class="dropdown-item">Mark as fixed</a>
</li>
@ -117,12 +117,12 @@
<table class="table table-compact compare-table">
<tr ng-repeat="alert in alertSummary.alerts | orderBy: ['-starred', 'title']" ng-show="alert.visible">
<td class="alert-checkbox">
<input type="checkbox" ng-disabled="!user.is_staff" ng-model="alert.selected" ng-change="alertSelected(alertSummary)"/>
<input type="checkbox" ng-disabled="!user.isStaff" ng-model="alert.selected" ng-change="alertSelected(alertSummary)"/>
</td>
<td class="alert-labels">
<a ng-attr-title="{{alert.starred ? 'Starred': 'Not starred'}}"
ng-class="{'fa fa-star visible': alert.starred, 'fa fa-star-o': !alert.starred}"
ng-disabled="!user.is_staff"
ng-disabled="!user.isStaff"
ng-click="alert.toggleStar()">
</a>
</td>

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

@ -99,10 +99,10 @@
<p class="text-muted" ng-if="!tooltipContent.alertSummary">
<span ng-if="!creatingAlert">
No alert
<span ng-if="user.is_staff">
(<a href="" ng-click="createAlert(tooltipContent)" ng-disabled="user.is_staff">create</a>)
<span ng-if="user.isStaff">
(<a href="" ng-click="createAlert(tooltipContent)" ng-disabled="user.isStaff">create</a>)
</span>
<span ng-if="!user.is_staff">
<span ng-if="!user.isStaff">
(log in as a a sheriff to create)
</span>
</span>

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

@ -1,7 +0,0 @@
<annotations-tab
classifications="classifications"
classification-types="classificationTypes"
selected-job="selectedJob"
bugs="bugs"
watch-depth="reference"
/>

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

@ -1,9 +0,0 @@
<autoclassify-tab
ng-if="tabService.tabs.autoClassification.enabled"
job="job"
logs-parsed="jobLogsAllParsed"
has-logs="job_log_urls.length > 0"
log-parse-status="logParseStatus"
autoclassify-status="job.autoclassify_status"
user="user"
/>

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

@ -1,676 +0,0 @@
import $ from 'jquery';
import _ from 'lodash';
import jsyaml from 'js-yaml';
import { Queue, slugid } from 'taskcluster-client-web';
import treeherder from '../js/treeherder';
import thTaskcluster from '../js/services/taskcluster';
import tcJobActionsTemplate from '../partials/main/tcjobactions.html';
import intermittentTemplate from '../partials/main/intermittent.html';
import { getStatus, isReftest } from '../helpers/job';
import { formatModelError, formatTaskclusterError } from '../helpers/errorMessage';
import {
getBugUrl,
getSlaveHealthUrl,
getInspectTaskUrl,
getLogViewerUrl,
getReftestUrl,
} from '../helpers/url';
import { thEvents } from "../js/constants";
import JobModel from '../models/job';
import JobDetailModel from '../models/jobDetail';
import TextLogStepModel from '../models/textLogStep';
import JobClassificationModel from '../models/classification';
import BugJobMapModel from '../models/bugJobMap';
import BugSuggestionsModel from '../models/bugSuggestions';
import JobLogUrlModel from '../models/jobLogUrl';
treeherder.controller('PluginCtrl', [
'$scope', '$rootScope', '$location', '$http', '$interpolate', '$uibModal',
'thClassificationTypes', 'dateFilter',
'numberFilter', 'thJobFilters',
'$q', 'thPinboard',
'thBuildApi', 'thNotify',
'thTabs', '$timeout', 'ThResultSetStore',
'PhSeries', 'tcactions',
function PluginCtrl(
$scope, $rootScope, $location, $http, $interpolate, $uibModal,
thClassificationTypes, dateFilter,
numberFilter, thJobFilters,
$q, thPinboard,
thBuildApi, thNotify, thTabs,
$timeout, ThResultSetStore, PhSeries,
tcactions) {
$scope.job = {};
$scope.revisionList = [];
// Show the Failure Summary tab, except if there's a URL parameter to enable Failure Classification one.
const showAutoClassifyTab = function () {
thTabs.tabs.autoClassification.enabled = $location.search().autoclassify === true;
};
showAutoClassifyTab();
$rootScope.$on('$locationChangeSuccess', function () {
showAutoClassifyTab();
});
$rootScope.$on('userChange', function () {
showAutoClassifyTab();
});
/**
* Set the tab options and selections based on the selected job.
* The default selected tab will be based on whether the job was a
* success or failure.
*
* Some tabs will be shown/hidden based on the job (such as Talos)
* and some based on query string params (such as autoClassification).
*
*/
const initializeTabs = function (job, hasPerformanceData) {
let successTab = "jobDetails";
let failTab = "failureSummary";
// Error Classification/autoclassify special handling
if ($scope.tabService.tabs.autoClassification.enabled) {
failTab = "autoClassification";
}
$scope.tabService.tabs.perfDetails.enabled = hasPerformanceData;
// the success tabs should be "performance" if job was not a build
const jobType = job.job_type_name;
if (hasPerformanceData && jobType !== "Build" && jobType !== "Nightly" &&
!jobType.startsWith('build-')) {
successTab = 'perfDetails';
}
if (getStatus(job) === 'success') {
$scope.tabService.selectedTab = successTab;
} else {
$scope.tabService.selectedTab = failTab;
}
};
$scope.loadBugSuggestions = function () {
$scope.errors = [];
BugSuggestionsModel.get($scope.job.id).then((suggestions) => {
suggestions.forEach(function (suggestion) {
suggestion.bugs.too_many_open_recent = (
suggestion.bugs.open_recent.length > $scope.bug_limit
);
suggestion.bugs.too_many_all_others = (
suggestion.bugs.all_others.length > $scope.bug_limit
);
suggestion.valid_open_recent = (
suggestion.bugs.open_recent.length > 0 &&
!suggestion.bugs.too_many_open_recent
);
suggestion.valid_all_others = (
suggestion.bugs.all_others.length > 0 &&
!suggestion.bugs.too_many_all_others &&
// If we have too many open_recent bugs, we're unlikely to have
// relevant all_others bugs, so don't show them either.
!suggestion.bugs.too_many_open_recent
);
});
// if we have no bug suggestions, populate with the raw errors from
// the log (we can do this asynchronously, it should normally be
// fast)
if (!suggestions.length) {
TextLogStepModel.get($scope.job.id).then((textLogSteps) => {
$scope.errors = textLogSteps
.filter(step => step.result !== 'success')
.map(function (step) {
return {
name: step.name,
result: step.result,
lvURL: getLogViewerUrl($scope.job.id, $rootScope.repoName, step.finished_line_number)
};
});
});
}
$scope.suggestions = suggestions;
$scope.bugSuggestionsLoading = false;
});
};
$scope.fileBug = function (index) {
const summary = $scope.suggestions[index].search;
const crashRegex = /application crashed \[@ (.+)\]$/g;
const crash = summary.match(crashRegex);
const crashSignatures = crash ? [crash[0].split("application crashed ")[1]] : [];
const allFailures = $scope.suggestions.map(sugg => (sugg.search.split(" | ")));
const modalInstance = $uibModal.open({
template: intermittentTemplate,
controller: 'BugFilerCtrl',
size: 'lg',
openedClass: "filer-open",
resolve: {
summary: () => (summary),
search_terms: () => ($scope.suggestions[index].search_terms),
fullLog: () => ($scope.job_log_urls[0].url),
parsedLog: () => ($scope.lvFullUrl),
reftest: () => ($scope.isReftest() ? $scope.reftestUrl : ""),
selectedJob: () => ($scope.selectedJob),
allFailures: () => (allFailures),
crashSignatures: () => (crashSignatures),
successCallback: () => (data) => {
// Auto-classify this failure now that the bug has been filed
// and we have a bug number
thPinboard.addBug({ id: data.success });
$rootScope.$evalAsync(
$rootScope.$emit(
thEvents.saveClassification));
// Open the newly filed bug in a new tab or window for further editing
window.open(getBugUrl(data.success));
}
}
});
thPinboard.pinJob($scope.selectedJob);
modalInstance.opened.then(function () {
window.setTimeout(() => modalInstance.initiate(), 0);
});
};
// this promise will void all the ajax requests
// triggered by selectJob once resolved
let selectJobController = null;
const selectJob = function (job) {
$scope.bugSuggestionsLoading = true;
// make super-extra sure that the autoclassify tab shows up when it should
showAutoClassifyTab();
// set the scope variables needed for the job detail panel
if (job.id) {
if (selectJobController) {
// Cancel the in-progress fetch requests.
selectJobController.abort();
}
// TODO: Remove this eslint-disable once we're on a newer version of eslint
// with globals that include AbortController.
// eslint-disable-next-line no-undef
selectJobController = new AbortController();
$scope.job_detail_loading = true;
$scope.job = {};
$scope.job_details = [];
const jobPromise = JobModel.get(
$scope.repoName,
job.id,
selectJobController.signal);
const jobDetailPromise = JobDetailModel.getJobDetails(
{ job_guid: job.job_guid },
selectJobController.signal);
const jobLogUrlPromise = JobLogUrlModel.getList(
{ job_id: job.id },
selectJobController.signal);
const phSeriesPromise = PhSeries.getSeriesData(
$scope.repoName, { job_id: job.id });
return $q.all([
jobPromise,
jobDetailPromise,
jobLogUrlPromise,
phSeriesPromise
]).then(function (results) {
//the first result comes from the job promise
$scope.job = results[0];
$scope.resultsetId = ThResultSetStore.getSelectedJob().job.result_set_id;
$scope.jobRevision = ThResultSetStore.getPush($scope.resultsetId).revision;
// the second result comes from the job detail promise
$scope.job_details = results[1];
// incorporate the buildername into the job details if this is a buildbot job
// (i.e. it has a buildbot request id)
const buildbotRequestIdDetail = _.find($scope.job_details,
{ title: 'buildbot_request_id' });
if (buildbotRequestIdDetail) {
$scope.job_details = $scope.job_details.concat({
title: "Buildername",
value: $scope.job.ref_data_name
});
$scope.buildernameIndex = $scope.job_details.findIndex(({ title }) => title === 'Buildername');
}
// the third result comes from the jobLogUrl promise
// exclude the json log URLs
$scope.job_log_urls = _.reject(
results[2],
function (log) {
return log.name.endsWith("_json");
});
// Provide a parse status as a scope variable for logviewer shortcut
if (!$scope.job_log_urls.length) {
$scope.logParseStatus = 'unavailable';
} else if ($scope.job_log_urls[0].parse_status) {
$scope.logParseStatus = $scope.job_log_urls[0].parse_status;
}
// Provide a parse status for the model
$scope.jobLogsAllParsed = ($scope.job_log_urls ?
$scope.job_log_urls.every(jlu => jlu.parse_status !== 'pending') :
false);
$scope.lvUrl = getLogViewerUrl($scope.job.id, $scope.repoName);
$scope.lvFullUrl = location.origin + "/" + $scope.lvUrl;
if ($scope.job_log_urls.length) {
$scope.reftestUrl = `${getReftestUrl($scope.job_log_urls[0].url)}&only_show_unexpected=1`;
}
const performanceData = (Object.values(results[3])).reduce((a, b) => [...a, ...b], []);
if (performanceData) {
const signatureIds = [...new Set(_.map(performanceData, 'signature_id'))];
$q.all(_.chunk(signatureIds, 20).map(
signatureIdChunk => PhSeries.getSeriesList($scope.repoName, { id: signatureIdChunk })
)).then((seriesListList) => {
const seriesList = seriesListList.reduce((a, b) => [...a, ...b], []);
$scope.perfJobDetail = performanceData.map(d => ({
series: seriesList.find(s => d.signature_id === s.id),
...d
})).filter(d => !d.series.parentSignature).map(d => ({
url: `/perf.html#/graphs?series=${[$scope.repoName, d.signature_id, 1, d.series.frameworkId]}&selected=${[$scope.repoName, d.signature_id, $scope.job.result_set_id, d.id]}`,
value: d.value,
title: d.series.name
}));
});
}
// set the tab options and selections based on the selected job
initializeTabs($scope.job, (Object.keys(performanceData).length > 0));
$scope.updateClassifications();
$scope.updateBugs();
$scope.loadBugSuggestions();
$scope.job_detail_loading = false;
}).finally(() => {
selectJobController = null;
});
}
};
$scope.getCountPinnedJobs = function () {
return thPinboard.count.numPinnedJobs;
};
$scope.getCountPinnedTitle = function () {
let title = "";
if (thPinboard.count.numPinnedJobs === 1) {
title = "You have " + thPinboard.count.numPinnedJobs + " job pinned";
} else if (thPinboard.count.numPinnedJobs > 1) {
title = "You have " + thPinboard.count.numPinnedJobs + " jobs pinned";
}
return title;
};
$scope.togglePinboardVisibility = function () {
$scope.isPinboardVisible = !$scope.isPinboardVisible;
};
const getRevisionTips = function (list) {
list.splice(0, list.length);
const rsArr = ThResultSetStore.getPushArray();
rsArr.forEach((rs) => {
list.push({
revision: rs.revision,
author: rs.author,
title: rs.revisions[0].comments.split('\n')[0]
});
});
};
$scope.$watch('getCountPinnedJobs()', function (newVal, oldVal) {
if (oldVal === 0 && newVal > 0) {
$scope.isPinboardVisible = true;
getRevisionTips($scope.revisionList);
}
});
$scope.canCancel = function () {
return $scope.job &&
($scope.job.state === "pending" || $scope.job.state === "running");
};
$scope.retriggerJob = function (jobs) {
if ($scope.user.loggedin) {
// Spin the retrigger button when retriggers happen
$("#retrigger-btn > span").removeClass("action-bar-spin");
window.requestAnimationFrame(function () {
window.requestAnimationFrame(function () {
$("#retrigger-btn > span").addClass("action-bar-spin");
});
});
const job_id_list = _.map(jobs, 'id');
// The logic here is somewhat complicated because we need to support
// two use cases the first is the case where we notify a system other
// then buildbot that a retrigger has been requested (eg mozilla-taskcluster).
// The second is when we have the buildapi id and need to send a request
// to the self serve api (which does not listen over pulse!).
JobModel.retrigger($scope.repoName, job_id_list).then(function () {
return JobDetailModel.getJobDetails({
title: "buildbot_request_id",
repository: $scope.repoName,
job_id__in: job_id_list.join(',')
}).then(function (data) {
const requestIdList = _.map(data, 'value');
requestIdList.forEach(function (requestId) {
thBuildApi.retriggerJob($scope.repoName, requestId);
});
});
}).then(function () {
$scope.$apply(thNotify.send("Retrigger request sent", "success"));
}, function (e) {
// Generic error eg. the user doesn't have LDAP access
$scope.$apply(thNotify.send(
formatModelError(e, "Unable to send retrigger"), 'danger'));
});
} else {
thNotify.send("Must be logged in to retrigger a job", 'danger');
}
};
$scope.backfillJob = function () {
if (!$scope.canBackfill()) {
return;
}
if (!$scope.user.loggedin) {
thNotify.send("Must be logged in to backfill a job", 'danger');
return;
}
if (!$scope.job.id) {
thNotify.send("Job not yet loaded for backfill", 'warning');
return;
}
if ($scope.job.build_system_type === 'taskcluster' || $scope.job.reason.startsWith('Created by BBB for task')) {
ThResultSetStore.getGeckoDecisionTaskId(
$scope.resultsetId).then(function (decisionTaskId) {
return tcactions.load(decisionTaskId, $scope.job).then((results) => {
const actionTaskId = slugid();
if (results) {
const backfilltask = _.find(results.actions, { name: 'backfill' });
// We'll fall back to actions.yaml if this isn't true
if (backfilltask) {
return tcactions.submit({
action: backfilltask,
actionTaskId,
decisionTaskId,
taskId: results.originalTaskId,
task: results.originalTask,
input: {},
staticActionVariables: results.staticActionVariables,
}).then(function () {
$scope.$apply(thNotify.send(`Request sent to backfill job via actions.json (${actionTaskId})`, 'success'));
}, function (e) {
// The full message is too large to fit in a Treeherder
// notification box.
$scope.$apply(thNotify.send(formatTaskclusterError(e), 'danger', { sticky: true }));
});
}
}
// Otherwise we'll figure things out with actions.yml
const queue = new Queue({ credentialAgent: thTaskcluster.getAgent() });
// buildUrl is documented at
// https://github.com/taskcluster/taskcluster-client-web#construct-urls
// It is necessary here because getLatestArtifact assumes it is getting back
// JSON as a reponse due to how the client library is constructed. Since this
// result is yml, we'll fetch it manually using $http and can use the url
// returned by this method.
const url = queue.buildUrl(
queue.getLatestArtifact,
decisionTaskId,
'public/action.yml'
);
$http.get(url).then(function (resp) {
let action = resp.data;
const template = $interpolate(action);
action = template({
action: 'backfill',
action_args: '--project=' + $scope.repoName + ' --job=' + $scope.job.id,
});
const task = thTaskcluster.refreshTimestamps(jsyaml.safeLoad(action));
queue.createTask(actionTaskId, task).then(function () {
$scope.$apply(thNotify.send(`Request sent to backfill job via actions.yml (${actionTaskId})`, 'success'));
}, function (e) {
// The full message is too large to fit in a Treeherder
// notification box.
$scope.$apply(thNotify.send(formatTaskclusterError(e), 'danger', { sticky: true }));
});
});
});
});
} else {
thNotify.send('Unable to backfill this job type!', 'danger', { sticky: true });
}
};
// Can we backfill? At the moment, this only ensures we're not in a 'try' repo.
$scope.canBackfill = function () {
return $scope.user.loggedin && $scope.currentRepo &&
!$scope.currentRepo.is_try_repo;
};
$scope.backfillButtonTitle = function () {
let title = "";
// Ensure currentRepo is available on initial page load
if (!$scope.currentRepo) {
// still loading
return undefined;
}
if (!$scope.user.loggedin) {
title = title.concat("must be logged in to backfill a job / ");
}
if ($scope.currentRepo.is_try_repo) {
title = title.concat("backfill not available in this repository");
}
if (title === "") {
title = "Trigger jobs of ths type on prior pushes " +
"to fill in gaps where the job was not run";
} else {
// Cut off trailing "/ " if one exists, capitalize first letter
title = title.replace(/\/ $/, "");
title = title.replace(/^./, l => l.toUpperCase());
}
return title;
};
$scope.cancelJobs = function (jobs) {
const jobIdsToCancel = jobs.filter(job => (job.state === "pending" ||
job.state === "running")).map(
job => job.id);
// get buildbot ids of any buildbot jobs we want to cancel
// first
JobDetailModel.getJobDetails({
job_id__in: jobIdsToCancel,
title: 'buildbot_request_id'
}).then(function (buildbotRequestIdDetails) {
return JobModel.cancel($scope.repoName, jobIdsToCancel).then(
function () {
buildbotRequestIdDetails.forEach(
function (buildbotRequestIdDetail) {
const requestId = parseInt(buildbotRequestIdDetail.value);
thBuildApi.cancelJob($scope.repoName, requestId);
});
});
}).then(function () {
thNotify.send("Cancel request sent", "success");
}).catch(function (e) {
thNotify.send(
formatModelError(e, "Unable to cancel job"),
"danger",
{ sticky: true }
);
});
};
$scope.cancelJob = function () {
$scope.cancelJobs([$scope.job]);
};
$scope.customJobAction = function () {
$uibModal.open({
template: tcJobActionsTemplate,
controller: 'TCJobActionsCtrl',
size: 'lg',
resolve: {
job: function () {
return $scope.job;
},
repoName: function () {
return $scope.repoName;
},
resultsetId: function () {
return $scope.resultsetId;
}
}
});
};
// Test to expose the reftest button in the job details navbar
$scope.isReftest = function () {
if ($scope.selectedJob) {
return isReftest($scope.selectedJob);
}
};
const selectJobAndRender = function (job) {
$scope.jobLoadedPromise = selectJob(job);
$('#info-panel').addClass('info-panel-slide');
$scope.jobLoadedPromise.then(() => {
thTabs.showTab(thTabs.selectedTab, job.id);
});
};
$rootScope.$on(thEvents.jobClick, function (event, job) {
selectJobAndRender(job);
$rootScope.selectedJob = job;
});
$rootScope.$on(thEvents.clearSelectedJob, function () {
if (selectJobController) {
// Cancel the in-progress fetch requests.
selectJobController.abort();
}
});
$rootScope.$on(thEvents.selectNextTab, function () {
// Establish the visible tabs for the job
const visibleTabs = [];
for (const i in thTabs.tabOrder) {
if (thTabs.tabs[thTabs.tabOrder[i]].enabled) {
visibleTabs.push(thTabs.tabOrder[i]);
}
}
// Establish where we are and increment one tab
let t = visibleTabs.indexOf(thTabs.selectedTab);
if (t === visibleTabs.length - 1) {
t = 0;
} else {
t++;
}
// Select that new tab
thTabs.showTab(visibleTabs[t], $scope.selectedJob.id);
});
$scope.bug_job_map_list = [];
$scope.classificationTypes = thClassificationTypes;
// load the list of existing classifications (including possibly a new one just
// added).
$scope.updateClassifications = function () {
JobClassificationModel.getList({ job_id: $scope.job.id }).then((response) => {
$timeout(() => {
$scope.classifications = response;
$scope.latestClassification = $scope.classifications[0];
});
});
};
// load the list of bug associations (including possibly new ones just
// added).
$scope.updateBugs = function () {
if (_.has($scope.job, "id")) {
BugJobMapModel.getList({ job_id: $scope.job.id }).then((response) => {
$timeout(() => {
$scope.bugs = response;
});
});
}
};
// Open the logviewer and provide notifications if it isn't available
$rootScope.$on(thEvents.openLogviewer, function () {
if ($scope.logParseStatus === 'pending') {
thNotify.send("Log parsing in progress, log viewer not yet available", 'info');
} else if ($scope.logParseStatus === 'failed') {
thNotify.send("Log parsing has failed, log viewer is unavailable", 'warning');
} else if ($scope.logParseStatus === 'unavailable') {
thNotify.send("No logs available for this job", 'info');
// If it's available open the logviewer
} else if ($scope.logParseStatus === 'parsed') {
$('#logviewer-btn')[0].click();
}
});
$rootScope.$on(thEvents.jobRetrigger, function (event, job) {
$scope.retriggerJob([job]);
});
$rootScope.$on(thEvents.jobsClassified, function () {
// use $timeout here so that all the other $digest operations related to
// the event of ``jobsClassified`` will be done. This will then
// be a new $digest cycle.
$timeout($scope.updateClassifications);
});
$rootScope.$on(thEvents.bugsAssociated, function () {
$timeout($scope.updateBugs);
});
$rootScope.$on(thEvents.autoclassifyVerified, function () {
// These operations are unneeded unless we verified the full job,
// But getting that information to here seems to be non-trivial
$timeout($scope.updateBugs);
$timeout($scope.updateClassifications);
ThResultSetStore.fetchJobs([$scope.job.id]);
// Emit an event indicating that a job has been classified, although
// it might in fact not have been
const jobs = {};
jobs[$scope.job.id] = $scope.job;
$rootScope.$emit(thEvents.jobsClassified, { jobs: jobs });
});
$scope.pinboard_service = thPinboard;
// expose the tab service properties on the scope
$scope.tabService = thTabs;
//fetch URLs
$scope.getBugUrl = getBugUrl;
$scope.getSlaveHealthUrl = getSlaveHealthUrl;
$scope.getInspectTaskUrl = getInspectTaskUrl;
}
]);

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

@ -1,14 +0,0 @@
<div>
<div class="w-100 h-100">
<failure-summary-tab
suggestions="suggestions"
file-bug="fileBug"
selected-job="selectedJob"
errors="errors"
bug-suggestions-loading="bugSuggestionsLoading"
logs="job_log_urls"
job-log-urls="job_log_urls"
log-parse-status="logParseStatus"
/>
</div>
</div>

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

@ -1,4 +0,0 @@
<job-details-tab
job-details="job_details"
buildername-index="buildernameIndex"
/>

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

@ -1,10 +0,0 @@
<div>
<div>
<performance-tab
ng-if="tabService.tabs.perfDetails.enabled"
perf-job-detail="perfJobDetail"
repo-name="repoName"
revision="jobRevision"
/>
</div>
</div>

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

@ -1,307 +0,0 @@
import $ from 'jquery';
import Mousetrap from 'mousetrap';
import treeherder from '../js/treeherder';
import { thEvents } from "../js/constants";
treeherder.controller('PinboardCtrl', [
'$scope', '$rootScope', '$document', '$timeout', 'thPinboard', 'thNotify',
function PinboardCtrl(
$scope, $rootScope, $document, $timeout, thPinboard, thNotify) {
$rootScope.$on(thEvents.toggleJobPin, function (event, job) {
$scope.toggleJobPin(job);
if (!$scope.$$phase) {
$scope.$digest();
}
});
$rootScope.$on(thEvents.jobPin, function (event, job) {
$scope.pinJob(job);
if (!$scope.$$phase) {
$scope.$digest();
}
});
$rootScope.$on(thEvents.addRelatedBug, function (event, job) {
$scope.pinJob(job);
$scope.toggleEnterBugNumber(true);
});
$rootScope.$on(thEvents.saveClassification, function () {
if ($scope.isPinboardVisible) {
$scope.save();
}
});
$rootScope.$on(thEvents.clearPinboard, function () {
if ($scope.isPinboardVisible) {
$scope.unPinAll();
}
});
$scope.toggleJobPin = function (job) {
thPinboard.toggleJobPin(job);
if (!$scope.selectedJob) {
$scope.viewJob(job);
}
};
$scope.pulsePinCount = function () {
$(".pin-count-group").addClass("pin-count-pulse");
$timeout(function () {
$(".pin-count-group").removeClass("pin-count-pulse");
}, 700);
};
// Triggered on pin api events eg. from the job details navbar
$rootScope.$on(thEvents.pulsePinCount, function () {
$scope.pulsePinCount();
});
$scope.pinJob = function (job) {
thPinboard.pinJob(job);
if (!$scope.selectedJob) {
$scope.viewJob(job);
}
$scope.pulsePinCount();
};
$scope.unPinJob = function (id) {
thPinboard.unPinJob(id);
};
$scope.addBug = function (bug) {
thPinboard.addBug(bug);
};
$scope.removeBug = function (id) {
thPinboard.removeBug(id);
};
$scope.unPinAll = function () {
thPinboard.unPinAll();
$scope.classification = thPinboard.createNewClassification();
};
$scope.save = function () {
let errorFree = true;
if ($scope.enteringBugNumber) {
// we should save this for the user, as they likely
// just forgot to hit enter. Returns false if invalid
errorFree = $scope.saveEnteredBugNumber();
if (!errorFree) {
thNotify.send("Please enter a valid bug number", "danger");
}
}
if (!$scope.canSaveClassifications() && $scope.user.loggedin) {
thNotify.send("Please classify this failure before saving", "danger");
errorFree = false;
}
if (!$scope.user.loggedin) {
thNotify.send("Must be logged in to save job classifications", "danger");
errorFree = false;
}
if (errorFree) {
$scope.classification.who = $scope.user.email;
const classification = $scope.classification;
thPinboard.save(classification);
$scope.completeClassification();
$scope.classification = thPinboard.createNewClassification();
// HACK: it looks like Firefox on Linux and Windows doesn't
// want to accept keyboard input after this change for some
// reason which I don't understand. Chrome (any platform)
// or Firefox on Mac works fine though.
document.activeElement.blur();
}
};
$scope.saveClassificationOnly = function () {
if ($scope.user.loggedin) {
$scope.classification.who = $scope.user.email;
thPinboard.saveClassificationOnly($scope.classification);
} else {
thNotify.send("Must be logged in to save job classifications", "danger");
}
};
$scope.saveBugsOnly = function () {
if ($scope.user.loggedin) {
thPinboard.saveBugsOnly();
} else {
thNotify.send("Must be logged in to save job classifications", "danger");
}
};
$scope.isSHAorCommit = function (str) {
return /^[a-f\d]{12,40}$/.test(str) || str.includes("hg.mozilla.org");
};
// If the pasted data is (or looks like) a 12 or 40 char SHA,
// or if the pasted data is an hg.m.o url, automatically select
// the "fixed by commit" classification type
$scope.pasteSHA = function (evt) {
const pastedData = evt.originalEvent.clipboardData.getData('text');
if ($scope.isSHAorCommit(pastedData)) {
$scope.classification.failure_classification_id = 2;
}
};
$scope.retriggerAllPinnedJobs = function () {
// pushing pinned jobs to a list.
$scope.retriggerJob(Object.values($scope.pinnedJobs));
};
$scope.cancelAllPinnedJobsTitle = function () {
if (!$scope.user.loggedin) {
return "Not logged in";
} else if (!$scope.canCancelAllPinnedJobs()) {
return "No pending / running jobs in pinboard";
}
return "Cancel all the pinned jobs";
};
$scope.canCancelAllPinnedJobs = function () {
const cancellableJobs = Object.values($scope.pinnedJobs).filter(
job => (job.state === 'pending' || job.state === 'running'));
return $scope.user.loggedin && cancellableJobs.length > 0;
};
$scope.cancelAllPinnedJobs = function () {
if (window.confirm('This will cancel all the selected jobs. Are you sure?')) {
$scope.cancelJobs(Object.values($scope.pinnedJobs));
}
};
$scope.canSaveClassifications = function () {
const thisClass = $scope.classification;
return $scope.hasPinnedJobs() && $scope.user.loggedin &&
(thPinboard.hasRelatedBugs() ||
(thisClass.failure_classification_id !== 4 && thisClass.failure_classification_id !== 2) ||
$rootScope.currentRepo.is_try_repo ||
$rootScope.currentRepo.repository_group.name === "project repositories" ||
(thisClass.failure_classification_id === 4 && thisClass.text.length > 0) ||
(thisClass.failure_classification_id === 2 && thisClass.text.length > 7));
};
// Facilitates Clear all if no jobs pinned to reset pinboard UI
$scope.pinboardIsDirty = function () {
return $scope.classification.text !== '' ||
thPinboard.hasRelatedBugs() ||
$scope.classification.failure_classification_id !== 4;
};
// Dynamic btn/anchor title for classification save
$scope.saveUITitle = function (category) {
let title = "";
if (!$scope.user.loggedin) {
title = title.concat("not logged in / ");
}
if (category === "classification") {
if (!$scope.canSaveClassifications()) {
title = title.concat("ineligible classification data / ");
}
if (!$scope.hasPinnedJobs()) {
title = title.concat("no pinned jobs");
}
// We don't check pinned jobs because the menu dropdown handles it
} else if (category === "bug") {
if (!$scope.hasRelatedBugs()) {
title = title.concat("no related bugs");
}
}
if (title === "") {
title = "Save " + category + " data";
} else {
// Cut off trailing "/ " if one exists, capitalize first letter
title = title.replace(/\/ $/, "");
title = title.replace(/^./, l => l.toUpperCase());
}
return title;
};
$scope.hasPinnedJobs = function () {
return thPinboard.hasPinnedJobs();
};
$scope.hasRelatedBugs = function () {
return thPinboard.hasRelatedBugs();
};
function handleRelatedBugDocumentClick(event) {
if (!$(event.target).hasClass("add-related-bugs-input")) {
$scope.$apply(function () {
if ($scope.newEnteredBugNumber) {
$scope.saveEnteredBugNumber();
} else {
$scope.toggleEnterBugNumber(false);
}
});
}
}
$scope.toggleEnterBugNumber = function (tf) {
$scope.enteringBugNumber = tf;
$scope.focusInput = tf;
$document.off('click', handleRelatedBugDocumentClick);
if (tf) {
// Rebind escape to canceling the bug entry, pressing escape
// again will close the pinboard as usual.
Mousetrap.bind('escape', function () {
const cancel = $scope.toggleEnterBugNumber.bind($scope, false);
$scope.$evalAsync(cancel);
});
// Install a click handler on the document so that clicking
// outside of the input field will close it. A blur handler
// can't be used because it would have timing issues with the
// click handler on the + icon.
$timeout(function () {
$document.on('click', handleRelatedBugDocumentClick);
}, 0);
} else {
$scope.newEnteredBugNumber = '';
}
};
$scope.completeClassification = function () {
$rootScope.$broadcast('blur-this', "classification-comment");
};
// The manual bug entry input eats the global ctrl+enter save() shortcut.
// Force that event to be emitted so ctrl+enter saves the classification.
$scope.ctrlEnterSaves = function (ev) {
if (ev.ctrlKey && ev.keyCode === 13) {
$scope.$evalAsync($rootScope.$emit(thEvents.saveClassification));
}
};
$scope.saveEnteredBugNumber = function () {
if ($scope.enteringBugNumber) {
if (!$scope.newEnteredBugNumber) {
$scope.toggleEnterBugNumber(false);
} else if (/^[0-9]*$/.test($scope.newEnteredBugNumber)) {
thPinboard.addBug({ id: $scope.newEnteredBugNumber });
$scope.toggleEnterBugNumber(false);
return true;
}
}
};
$scope.viewJob = function (job) {
$rootScope.selectedJob = job;
$rootScope.$emit(thEvents.jobClick, job);
$rootScope.$emit(thEvents.selectJob, job);
};
$scope.classification = thPinboard.createNewClassification();
$scope.pinnedJobs = thPinboard.pinnedJobs;
$scope.relatedBugs = thPinboard.relatedBugs;
}
]);

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

@ -1,203 +0,0 @@
<div id="info-panel-resizer"
resizer="horizontal"
resizer-height="6"
resizer-bottom="#info-panel">
</div>
<div ng-controller="PinboardCtrl"
id="pinboard-panel"
ng-show="isPinboardVisible"
ng-include src="'partials/main/thPinboardPanel.html'">
</div>
<div id="info-panel-content">
<div id="job-details-panel">
<div id="job-details-actionbar">
<nav class="navbar navbar-dark info-panel-navbar">
<ul class="nav navbar-nav actionbar-nav">
<li ng-repeat="job_log_url in job_log_urls">
<a ng-if="job_log_url.parse_status == 'parsed'"
id="logviewer-btn"
title="Open the log viewer in a new window"
target="_blank" rel="noopener"
href="{{::lvUrl}}"
copy-value="{{::lvFullUrl}}"
class="">
<img src="../img/logviewerIcon.svg"
class="logviewer-icon"><img>
</a>
<a ng-if="job_log_url.parse_status == 'failed'"
id="logviewer-btn"
title="Log parsing has failed"
class="disabled" >
<img src="../img/logviewerIcon.svg"
class="logviewer-icon"><img>
</a>
<a ng-if="job_log_url.parse_status == 'pending'"
id="logviewer-btn"
class="disabled"
title="Log parsing in progress">
<img src="../img/logviewerIcon.svg"
class="logviewer-icon"><img>
</a>
</li>
<li>
<a ng-if="!job_log_urls.length"
id="logviewer-btn"
class="disabled"
title="No logs available for this job">
<img src="../img/logviewerIcon.svg"
class="logviewer-icon"><img>
</a>
</li>
<li ng-repeat="job_log_url in job_log_urls">
<a id="raw-log-btn"
class="raw-log-icon"
title="Open the raw log in a new window"
target="_blank" rel="noopener"
href="{{::job_log_url.url}}"
copy-value="{{::job_log_url.url}}">
<span class="fa fa-file-text-o"></span>
</a>
</li>
<li>
<a ng-if="!job_log_urls.length"
class="disabled raw-log-icon"
title="No logs available for this job">
<span class="fa fa-file-text-o"></span>
</a>
</li>
<li>
<a id="pin-job-btn" href=""
title="Add this job to the pinboard"
class="icon-blue"
ng-click="pinboard_service.pinJob(selectedJob)">
<span class="fa fa-thumb-tack"></span>
</a>
</li>
<li>
<button id="retrigger-btn" href=""
ng-attr-title="{{user.loggedin ? 'Repeat the selected job' :
'Must be logged in to retrigger a job'}}"
ng-class="user.loggedin ? 'icon-green' : 'disabled'"
ng-disabled="!user.loggedin"
ng-click="retriggerJob([selectedJob])">
<span class="fa fa-repeat"></span>
</button>
</li>
<li ng-if="isReftest()" ng-repeat="job_log_url in job_log_urls">
<a title="Launch the Reftest Analyser in a new window"
target="_blank" rel="noopener"
href="{{::reftestUrl}}">
<span class="fa fa-bar-chart-o"></span>
</a>
</li>
<li ng-show="canCancel()">
<a ng-attr-title="{{user.loggedin ? 'Cancel this job' :
'Must be logged in to cancel a job'}}"
ng-class="user.loggedin ? 'hover-warning' : 'disabled'"
href=""
ng-click="cancelJob()">
<span class="fa fa-times-circle cancel-job-icon"></span>
</a>
</li>
</ul>
<ul class="nav navbar-right">
<li class="dropdown">
<span id="actionbar-menu-btn"
title="Other job actions"
aria-haspopup="true" aria-expanded="false"
class="dropdown-toggle"
type="button"
data-toggle="dropdown">
<span class="fa fa-ellipsis-h" aria-hidden="true"></span>
</span>
<ul class="dropdown-menu actionbar-menu" role="menu">
<li>
<button id="backfill-btn" href=""
class="dropdown-item"
ng-class="!user.loggedin || !canBackfill() ? 'disabled' : ''"
ng-attr-title="{{ backfillButtonTitle() }}"
ng-disabled="!canBackfill()"
ng-click="!canBackfill() || backfillJob()">Backfill</button>
</li>
<li ng-if-start="job.taskcluster_metadata">
<a target="_blank" rel="noopener" class="dropdown-item" href="{{ getInspectTaskUrl(job.taskcluster_metadata.task_id) }}">Inspect Task</a>
</li>
<li><a target="_blank" rel="noopener" class="dropdown-item" href="{{ getInspectTaskUrl(job.taskcluster_metadata.task_id) }}/create">Edit and Retrigger</a>
</li>
<li>
<a target="_blank" rel="noopener" class="dropdown-item" href="https://tools.taskcluster.net/tasks/{{job.taskcluster_metadata.task_id}}/interactive">Create Interactive Task</a>
</li>
<li ng-if-end>
<a ng-click="customJobAction()" class="dropdown-item">Custom Action...</a>
</li>
</ul>
</li>
</ul>
</nav>
</div>
<div id="job-details-pane">
<summary-panel
name="SummaryPanel"
classification="latestClassification"
job="job"
bugs="bugs"
job-log-urls="job_log_urls"
classification-types="classificationTypes"
job-detail-loading="job_detail_loading"
repo-name="repoName"
build-url="buildUrl"
/>
</div>
</div>
<div id="job-tabs-panel">
<div id="job-tabs-navbar">
<nav class="info-panel-navbar info-panel-navbar-tabs">
<ul class="nav navbar-nav tab-headers"
ng-class="{'perf-job-selected': tabService.tabs.perfDetails.enabled}">
<li ng-repeat="tabName in tabService.tabOrder"
ng-if="tabService.tabs[tabName].enabled"
ng-class="{'active': tabService.selectedTab == tabName}">
<a title="Show {{ tabService.tabs[tabName].description }}"
href=""
ng-click="tabService.showTab(tabName, job.id)">
{{ tabService.tabs[tabName].title }}
</a>
</li>
</ul>
<ul class="nav navbar-nav info-panel-navbar-controls">
<li ng-click="togglePinboardVisibility()"
id="pinboard-btn">
<div ng-attr-title="{{isPinboardVisible ? 'Close the pinboard' : 'Open the pinboard'}}"
class="pinboard-btn-text">Pinboard
<div ng-if="pinboard_service.count.numPinnedJobs"
title="{{getCountPinnedTitle()}}"
class="pin-count-group"
ng-class="{'pin-count-group-3-digit': (pinboard_service.count.numPinnedJobs > 99)}">
<div class="pin-count-text"
ng-class="{'pin-count-text-3-digit': (pinboard_service.count.numPinnedJobs > 99)}">
{{getCountPinnedJobs()}}</div>
</div>
<span class="fa" ng-class="isPinboardVisible ? 'fa-angle-down' : 'fa-angle-up'"></span>
</div>
</li>
<li>
<a title="Close the job panel" href="" ng-click="closeJob()">
<span class="fa fa-times"></span>
</a>
</li>
</ul>
</nav>
</div>
<div id="job-tabs-pane">
<div ng-repeat="(tabId, tab) in tabService.tabs"
ng-show="tabId == tabService.selectedTab"
class="job-tabs-divider">
<ng-include src="tab.content"></ng-include>
</div>
</div>
</div>
</div>
<div id="clipboard-container"><textarea id="clipboard"></textarea></div>

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

@ -1,3 +0,0 @@
<similar-jobs-tab
repo-name="repoName"
/>

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

@ -1,74 +0,0 @@
import angular from 'angular';
import treeherder from '../js/treeherder';
treeherder.factory('thTabs', [
function () {
const thTabs = {
tabs: {
jobDetails: {
title: "Job Details",
description: "additional job information",
content: "plugins/job_details/main.html",
enabled: true
},
failureSummary: {
title: "Failure Summary",
description: "failure summary",
content: "plugins/failure_summary/main.html",
enabled: true
},
autoClassification: {
title: "Failure Classification",
description: "intermittent classification interface",
content: "plugins/auto_classification/main.html",
enabled: false
},
annotations: {
title: "Annotations",
description: "annotations",
content: "plugins/annotations/main.html",
enabled: true
},
similarJobs: {
title: "Similar Jobs",
description: "similar jobs",
content: "plugins/similar_jobs/main.html",
enabled: true
},
perfDetails: {
title: "Performance",
description: "performance details",
content: "plugins/perf_details/main.html",
enabled: false
}
},
tabOrder: [
"jobDetails",
"failureSummary",
"autoClassification",
"annotations",
"similarJobs",
"perfDetails"
],
selectedTab: "jobDetails",
showTab: function (tab, contentId) {
thTabs.selectedTab = tab;
if (!thTabs.tabs[thTabs.selectedTab].enabled) {
thTabs.selectedTab = 'jobDetails';
}
// if the tab exposes an update function, call it
// only refresh the tab if the content hasn't been loaded yet
// or we don't have an identifier for the content loaded
if (angular.isUndefined(thTabs.tabs[thTabs.selectedTab].contentId) ||
thTabs.tabs[thTabs.selectedTab].contentId !== contentId) {
if (angular.isFunction(thTabs.tabs[thTabs.selectedTab].update)) {
thTabs.tabs[thTabs.selectedTab].contentId = contentId;
thTabs.tabs[thTabs.selectedTab].update();
}
}
}
};
return thTabs;
}
]);

48
ui/vendor/resizer.js поставляемый
Просмотреть файл

@ -1,48 +0,0 @@
import angular from 'angular';
/* From: http://stackoverflow.com/a/22253161/295132 (author: Mario Campa) */
angular.module('mc.resizer', []).directive('resizer', ['$document', function($document) {
return function($scope, $element, $attrs) {
$element.on('mousedown', function(event) {
event.preventDefault();
$document.on('mousemove', mousemove);
$document.on('mouseup', mouseup);
});
function mousemove(event) {
if ($attrs.resizer == 'vertical') {
// Handle vertical resizer
var x = event.pageX;
if ($attrs.resizerMax && x > $attrs.resizerMax) {
x = parseInt($attrs.resizerMax);
}
$element.css({
left: x + 'px'
});
$($attrs.resizerLeft).css({
width: x + 'px'
});
$($attrs.resizerRight).css({
left: (x + parseInt($attrs.resizerWidth)) + 'px'
});
} else {
// Handle horizontal resizer
var y = window.innerHeight - event.pageY;
$element.css({
bottom: y + 'px'
});
$($attrs.resizerTop).css({
bottom: (y + parseInt($attrs.resizerHeight)) + 'px'
});
$($attrs.resizerBottom).css({
height: y + 'px'
});
}
}
function mouseup() {
$document.unbind('mousemove', mousemove);
$document.unbind('mouseup', mouseup);
}
};
}]);
export default 'mc.resizer';

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

@ -1577,7 +1577,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5:
classnames@^2.2.0, classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
@ -5963,7 +5963,7 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
prop-types@15.6.1, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1:
prop-types@15.6.1, prop-types@^15.5.0, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1:
version "15.6.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca"
dependencies:
@ -6242,6 +6242,13 @@ react-table@6.8.6:
dependencies:
classnames "^2.2.5"
react-tabs@2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-2.2.2.tgz#2f2935da379889484751d1df47c1b639e5ee835d"
dependencies:
classnames "^2.2.0"
prop-types "^15.5.0"
react-test-renderer@^16.0.0-0:
version "16.4.1"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.4.1.tgz#f2fb30c2c7b517db6e5b10ed20bb6b0a7ccd8d70"