зеркало из https://github.com/mozilla/treeherder.git
Bug 1450022 - Convert the rest of Details Panel to ReactJS (#3621)
This commit is contained in:
Родитель
8114711e2e
Коммит
15721f009c
|
@ -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>:
|
||||
<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;
|
||||
}
|
||||
]);
|
|
@ -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';
|
11
yarn.lock
11
yarn.lock
|
@ -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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче