зеркало из https://github.com/mozilla/treeherder.git
Bug 1450032 - Convert bottom (secondary) nav bar to ReactJS (#3779)
Also convert term resultState back to resultStatus I had wanted to migrate to using "resultState" instead, as it seemed more descriptive of what it is. But the filter params are using "resultStatus" and it would not be worth the effort to migrate. It doesn't really matter, but I want to be consistent to remove confusion, so moving these terms back to "resultStatus"-ish names.
This commit is contained in:
Родитель
4ccadb29f4
Коммит
bc4e8a7b14
|
@ -61,6 +61,7 @@
|
|||
"react-fontawesome": "1.6.1",
|
||||
"react-highlight-words": "0.11.0",
|
||||
"react-hot-loader": "3.1.3",
|
||||
"react-linkify": "0.2.2",
|
||||
"react-redux": "5.0.7",
|
||||
"react-router-dom": "4.3.1",
|
||||
"react-select": "1.2.1",
|
||||
|
|
|
@ -21,6 +21,8 @@ class Base(Page):
|
|||
# Initially try to compare with the text of the menu item.
|
||||
# But if there's an image instead of just text, then compare the
|
||||
# ``alt`` property of the image instead.
|
||||
self.wait.until(lambda _: self.is_element_displayed(
|
||||
*self._app_menu_locator))
|
||||
menu = self.find_element(*self._app_menu_locator).text
|
||||
return menu if menu else self.find_element(
|
||||
*self._app_logo_locator).get_attribute("alt")
|
||||
|
|
|
@ -14,7 +14,7 @@ class Treeherder(Base):
|
|||
|
||||
URL_TEMPLATE = '/#/jobs?repo={}'.format(settings.TREEHERDER_TEST_REPOSITORY_NAME)
|
||||
|
||||
_active_watched_repo_locator = (By.CSS_SELECTOR, '#watched-repo-navbar button.active')
|
||||
_active_watched_repo_locator = (By.CSS_SELECTOR, '#watched-repo-navbar a.active')
|
||||
_clear_filter_locator = (By.ID, 'quick-filter-clear-button')
|
||||
_filter_failures_locator = (By.CSS_SELECTOR, '.btn-nav-filter[title=failures]')
|
||||
_filter_in_progress_locator = (By.CSS_SELECTOR, '.btn-nav-filter[title*=progress]')
|
||||
|
@ -31,7 +31,7 @@ class Treeherder(Base):
|
|||
_repo_menu_locator = (By.ID, 'repoLabel')
|
||||
_pushes_locator = (By.CSS_SELECTOR, '.push:not(.row)')
|
||||
_unclassified_filter_locator = (By.CSS_SELECTOR, '.btn-unclassified-failures')
|
||||
_watched_repos_locator = (By.CSS_SELECTOR, '#watched-repo-navbar th-watched-repo')
|
||||
_watched_repos_locator = (By.CSS_SELECTOR, '#watched-repo-navbar .watched-repos')
|
||||
|
||||
@property
|
||||
def loaded(self):
|
||||
|
@ -43,6 +43,8 @@ class Treeherder(Base):
|
|||
|
||||
@property
|
||||
def active_watched_repo(self):
|
||||
self.wait.until(lambda _: self.is_element_displayed(
|
||||
*self._active_watched_repo_locator))
|
||||
return self.find_element(*self._active_watched_repo_locator).text
|
||||
|
||||
@property
|
||||
|
|
|
@ -8,6 +8,7 @@ def test_set_as_top_of_range(base_url, selenium, test_job):
|
|||
datestamp = pushes[1].datestamp
|
||||
assert pushes[0].datestamp != datestamp
|
||||
pushes[1].set_as_top_of_range()
|
||||
page.wait.until(lambda _: len(page.pushes))
|
||||
assert page.pushes[0].datestamp == datestamp
|
||||
|
||||
|
||||
|
@ -18,4 +19,5 @@ def test_set_as_bottom_of_range(base_url, selenium, test_job):
|
|||
datestamp = pushes[-2].datestamp
|
||||
assert pushes[-1].datestamp != datestamp
|
||||
page.pushes[-2].set_as_bottom_of_range()
|
||||
page.wait.until(lambda _: len(page.pushes))
|
||||
assert page.pushes[-1].datestamp == datestamp
|
||||
|
|
|
@ -5,6 +5,7 @@ def test_open_single_result(base_url, selenium, test_commit):
|
|||
page = Treeherder(selenium, base_url).open()
|
||||
page.wait.until(lambda _: 1 == len(page.pushes))
|
||||
page.pushes[0].view()
|
||||
page.wait.until(lambda _: len(page.pushes))
|
||||
assert 1 == len(page.pushes)
|
||||
assert test_commit.author == page.pushes[0].author
|
||||
assert test_commit.push.time.strftime('%a, %b %-d, %H:%M:%S') == page.pushes[0].datestamp
|
||||
|
|
|
@ -10,12 +10,6 @@ describe('linkifyBugs filter', function() {
|
|||
expect(linkifyBugs('Bug 123456'))
|
||||
.toEqual('Bug <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=123456" data-bugid="123456" title="bugzilla.mozilla.org">123456</a>');
|
||||
});
|
||||
|
||||
it('linkifies a PR', function() {
|
||||
var linkifyBugs = $filter('linkifyBugs');
|
||||
expect(linkifyBugs('PR#123456'))
|
||||
.toEqual('PR#<a href="https://github.com/mozilla-b2g/gaia/pull/123456" data-prid="123456" title="github.com">123456</a>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRevisionUrl filter', function() {
|
||||
|
@ -32,19 +26,6 @@ describe('getRevisionUrl filter', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('stripHtml filter', function() {
|
||||
var $filter;
|
||||
beforeEach(angular.mock.module('treeherder'));
|
||||
beforeEach(inject(function(_$filter_) {
|
||||
$filter = _$filter_;
|
||||
}));
|
||||
|
||||
it('deletes html tags', function() {
|
||||
var stripHtml = $filter('stripHtml');
|
||||
expect(stripHtml('My <html is> deleted')).toEqual('My deleted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('displayNumber filter', function() {
|
||||
var $filter;
|
||||
beforeEach(angular.mock.module('treeherder'));
|
||||
|
|
|
@ -136,6 +136,10 @@ login {
|
|||
* Left hand lower navbar
|
||||
*/
|
||||
|
||||
secondary-nav-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.watched-repo-main-btn {
|
||||
border-right: 0;
|
||||
padding-right: 5px;
|
||||
|
|
|
@ -25,6 +25,7 @@ import './js/treeherder_app';
|
|||
|
||||
// Treeherder React UI
|
||||
import './job-view/JobView';
|
||||
import './job-view/headerbars/SecondaryNavBar';
|
||||
|
||||
// Treeherder JS
|
||||
import './js/components/auth';
|
||||
|
@ -44,7 +45,6 @@ import './js/models/perf/series';
|
|||
import './js/controllers/main';
|
||||
import './js/controllers/repository';
|
||||
import './js/controllers/notification';
|
||||
import './js/controllers/filters';
|
||||
import './js/controllers/bugfiler';
|
||||
import './js/controllers/tcjobactions';
|
||||
import './js/filters';
|
||||
|
|
|
@ -29,8 +29,8 @@ export const getStatus = function getStatus(job) {
|
|||
// Get the CSS class for job buttons as well as jobs that show in the pinboard.
|
||||
// These also apply to result "groupings" like ``failures`` and ``in progress``
|
||||
// for the colored filter chicklets on the nav bar.
|
||||
export const getBtnClass = function getBtnClass(resultState, failureClassificationId) {
|
||||
let btnClass = btnClasses[resultState] || 'btn-default';
|
||||
export const getBtnClass = function getBtnClass(resultStatus, failureClassificationId) {
|
||||
let btnClass = btnClasses[resultStatus] || 'btn-default';
|
||||
|
||||
// handle if a job is classified
|
||||
const classificationId = parseInt(failureClassificationId, 10);
|
||||
|
|
|
@ -88,9 +88,9 @@ export default class JobButtonComponent extends React.Component {
|
|||
job_type_symbol, result } = job;
|
||||
|
||||
if (!visible) return null;
|
||||
const resultState = state === 'completed' ? result : state;
|
||||
const resultStatus = state === 'completed' ? result : state;
|
||||
const runnable = state === 'runnable';
|
||||
const btnClass = getBtnClass(resultState, failure_classification_id);
|
||||
const btnClass = getBtnClass(resultStatus, failure_classification_id);
|
||||
let title = `${job_type_name} - ${status}`;
|
||||
|
||||
if (state === 'completed') {
|
||||
|
|
|
@ -0,0 +1,296 @@
|
|||
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 { getBtnClass } from '../../helpers/job';
|
||||
import { getUrlParam } from '../../helpers/location';
|
||||
import WatchedRepo from './WatchedRepo';
|
||||
|
||||
class SecondaryNavBar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { $injector } = this.props;
|
||||
this.ThResultSetStore = $injector.get('ThResultSetStore');
|
||||
this.ThRepositoryModel = $injector.get('ThRepositoryModel');
|
||||
this.thJobFilters = $injector.get('thJobFilters');
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
this.$location = $injector.get('$location');
|
||||
|
||||
this.filterChicklets = [
|
||||
'failures',
|
||||
this.thJobFilters.filterGroups.nonfailures,
|
||||
'in progress'].reduce((acc, val) => acc.concat(val), []);
|
||||
const searchStr = this.thJobFilters.getFieldFiltersObj().searchStr;
|
||||
this.repoName = getUrlParam('repo');
|
||||
|
||||
this.state = {
|
||||
groupsExpanded: getUrlParam('group_state') === 'expanded',
|
||||
showDuplicateJobs: getUrlParam('duplicate_jobs') === 'visible',
|
||||
resultStatusFilters: this.thJobFilters.getResultStatusArray(),
|
||||
searchQueryStr: searchStr ? searchStr.join(' ') : '',
|
||||
watchedRepos: [],
|
||||
allUnclassifiedFailureCount: 0,
|
||||
filteredUnclassifiedFailureCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.toggleGroupState = this.toggleGroupState.bind(this);
|
||||
this.toggleFieldFilterVisible = this.toggleFieldFilterVisible.bind(this);
|
||||
this.toggleUnclassifiedFailures = this.toggleUnclassifiedFailures.bind(this);
|
||||
this.clearFilterBox = this.clearFilterBox.bind(this);
|
||||
this.unwatchRepo = this.unwatchRepo.bind(this);
|
||||
|
||||
this.unlistenGlobalFilterChanged = this.$rootScope.$on(thEvents.globalFilterChanged, () => {
|
||||
this.updateToggleFilters();
|
||||
this.setState({ searchQueryStr: this.getSearchStr() });
|
||||
});
|
||||
this.unlistenRepositoriesLoaded = this.$rootScope.$on(thEvents.repositoriesLoaded, () => (
|
||||
this.setState({ watchedRepos: this.ThRepositoryModel.watchedRepos })
|
||||
));
|
||||
this.unlistenJobsLoaded = this.$rootScope.$on(thEvents.jobsLoaded, () => (
|
||||
this.setState({
|
||||
allUnclassifiedFailureCount: this.ThResultSetStore.getAllUnclassifiedFailureCount(),
|
||||
filteredUnclassifiedFailureCount: this.ThResultSetStore.getFilteredUnclassifiedFailureCount(),
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unlistenGlobalFilterChanged();
|
||||
this.unlistenRepositoriesLoaded();
|
||||
this.unlistenJobsLoaded();
|
||||
}
|
||||
|
||||
getSearchStr() {
|
||||
const searchStr = this.thJobFilters.getFieldFiltersObj().searchStr;
|
||||
return searchStr ? searchStr.join(' ') : '';
|
||||
}
|
||||
|
||||
setSearchStr(ev) {
|
||||
this.setState({ searchQueryStr: ev.target.value });
|
||||
}
|
||||
|
||||
search(ev) {
|
||||
const value = ev.target.value;
|
||||
const filterVal = value === '' ? null : value;
|
||||
|
||||
if (ev.keyCode === 13) { // User hit enter
|
||||
this.thJobFilters.replaceFilter('searchStr', filterVal);
|
||||
ev.target.blur();
|
||||
this.$rootScope.$apply();
|
||||
}
|
||||
}
|
||||
|
||||
isFilterOn(filter) {
|
||||
const { resultStatusFilters } = this.state;
|
||||
const filterGroups = this.thJobFilters.filterGroups;
|
||||
|
||||
if (filter in filterGroups) {
|
||||
return filterGroups[filter].some(val => resultStatusFilters.includes(val));
|
||||
}
|
||||
return resultStatusFilters.includes(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle toggling one of the individual result status filter chicklets
|
||||
* on the nav bar
|
||||
*/
|
||||
toggleResultStatusFilterChicklet(filter) {
|
||||
const filterGroups = this.thJobFilters.filterGroups;
|
||||
const filterValues = filter in filterGroups ?
|
||||
filterGroups[filter] : // this is a filter grouping, so toggle all on/off
|
||||
[filter];
|
||||
|
||||
this.thJobFilters.toggleResultStatuses(filterValues);
|
||||
this.$rootScope.$apply();
|
||||
this.setState({ resultStatusFilters: this.thJobFilters.getResultStatusArray() });
|
||||
}
|
||||
|
||||
toggleFieldFilterVisible() {
|
||||
this.$rootScope.$emit(thEvents.toggleFieldFilterVisible);
|
||||
}
|
||||
|
||||
updateToggleFilters() {
|
||||
const classifiedState = this.thJobFilters.getClassifiedStateArray();
|
||||
|
||||
this.setState({
|
||||
resultStatusFilters: this.thJobFilters.getResultStatusArray(),
|
||||
classifiedFilter: classifiedState.includes('classified'),
|
||||
unClassifiedFilter: classifiedState.includes('unclassified'),
|
||||
});
|
||||
}
|
||||
|
||||
toggleShowDuplicateJobs() {
|
||||
const { showDuplicateJobs } = this.state;
|
||||
const newShowDuplicateJobs = showDuplicateJobs ? null : 'visible';
|
||||
|
||||
this.setState({ showDuplicateJobs: !showDuplicateJobs });
|
||||
this.$location.search('duplicate_jobs', newShowDuplicateJobs);
|
||||
this.$rootScope.$emit(thEvents.duplicateJobsVisibilityChanged);
|
||||
this.$rootScope.$apply();
|
||||
}
|
||||
|
||||
toggleGroupState() {
|
||||
const { groupsExpanded } = this.state;
|
||||
const newGroupState = groupsExpanded ? null : 'expanded';
|
||||
|
||||
this.setState({ groupsExpanded: !groupsExpanded });
|
||||
this.$location.search('group_state', newGroupState);
|
||||
this.$rootScope.$emit(thEvents.groupStateChanged, newGroupState);
|
||||
this.$rootScope.$apply();
|
||||
}
|
||||
|
||||
toggleUnclassifiedFailures() {
|
||||
this.thJobFilters.toggleUnclassifiedFailures();
|
||||
this.$rootScope.$apply();
|
||||
}
|
||||
|
||||
clearFilterBox() {
|
||||
this.setState({ searchQueryStr: '' });
|
||||
this.thJobFilters.removeFilter('searchStr');
|
||||
}
|
||||
|
||||
unwatchRepo(name) {
|
||||
this.ThRepositoryModel.unwatchRepo(name);
|
||||
this.setState({ watchedRepos: this.ThRepositoryModel.watchedRepos });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { updateButtonClick, serverChanged, $injector } = this.props;
|
||||
const {
|
||||
watchedRepos, groupsExpanded, showDuplicateJobs, searchQueryStr,
|
||||
allUnclassifiedFailureCount, filteredUnclassifiedFailureCount,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="watched-repo-navbar"
|
||||
className="th-context-navbar navbar-dark watched-repo-navbar"
|
||||
>
|
||||
<span className="justify-content-between w-100 d-flex flex-wrap">
|
||||
<span className="d-flex push-left watched-repos">
|
||||
{watchedRepos.map(watchedRepo => (
|
||||
<WatchedRepo
|
||||
key={watchedRepo}
|
||||
watchedRepo={watchedRepo}
|
||||
repoName={this.repoName}
|
||||
$injector={$injector}
|
||||
unwatchRepo={this.unwatchRepo}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
<form role="search" className="form-inline flex-row">
|
||||
{serverChanged && <span
|
||||
className="btn btn-sm btn-view-nav nav-menu-btn"
|
||||
onClick={updateButtonClick}
|
||||
id="revisionChangedLabel"
|
||||
title="New version of Treeherder has been deployed. Reload to pick up changes."
|
||||
>
|
||||
<span className="fa fa-exclamation-circle" /> Treeherder update available
|
||||
</span>}
|
||||
|
||||
{/* Unclassified Failures Button */}
|
||||
<span
|
||||
className={`btn btn-sm ${allUnclassifiedFailureCount ? 'btn-unclassified-failures' : 'btn-view-nav'}`}
|
||||
title="Loaded failures / toggle filtering for unclassified failures"
|
||||
tabIndex="-1"
|
||||
role="button"
|
||||
onClick={this.toggleUnclassifiedFailures}
|
||||
>
|
||||
<span id="unclassified-failure-count">{allUnclassifiedFailureCount}</span> unclassified
|
||||
</span>
|
||||
|
||||
{/* Filtered Unclassified Failures Button */}
|
||||
{filteredUnclassifiedFailureCount !== allUnclassifiedFailureCount &&
|
||||
<span
|
||||
className="navbar-badge badge badge-secondary badge-pill"
|
||||
title="Reflects the unclassified failures which pass the current filters"
|
||||
>
|
||||
<span id="filtered-unclassified-failure-count">{filteredUnclassifiedFailureCount}</span>
|
||||
</span>}
|
||||
|
||||
{/* Toggle Duplicate Jobs */}
|
||||
<span
|
||||
className={`btn btn-view-nav btn-sm btn-toggle-duplicate-jobs ${groupsExpanded ? 'disabled' : ''} ${!showDuplicateJobs ? 'strikethrough' : ''}`}
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
title={showDuplicateJobs ? 'Hide duplicate jobs' : 'Show duplicate jobs'}
|
||||
onClick={() => !groupsExpanded && this.toggleShowDuplicateJobs()}
|
||||
/>
|
||||
<span className="btn-group">
|
||||
{/* Toggle Group State Button */}
|
||||
<span
|
||||
className="btn btn-view-nav btn-sm btn-toggle-group-state"
|
||||
tabIndex="-1"
|
||||
role="button"
|
||||
title={groupsExpanded ? 'Collapse job groups' : 'Expand job groups'}
|
||||
onClick={() => this.toggleGroupState()}
|
||||
>( <span className="group-state-nav-icon">{groupsExpanded ? '-' : '+'}</span> )
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{/* Result Status Filter Chicklets */}
|
||||
<span className="resultStatusChicklets">
|
||||
<span id="filter-chicklets">
|
||||
{this.filterChicklets.map(filterName => (<span key={filterName}>
|
||||
<span
|
||||
className={`btn btn-view-nav btn-sm btn-nav-filter ${getBtnClass(filterName)}-filter-chicklet fa ${this.isFilterOn(filterName) ? 'fa-dot-circle-o' : 'fa-circle-thin'}`}
|
||||
onClick={() => this.toggleResultStatusFilterChicklet(filterName)}
|
||||
title={filterName}
|
||||
/>
|
||||
</span>))}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span
|
||||
className="btn btn-view-nav btn-sm"
|
||||
onClick={() => this.toggleFieldFilterVisible()}
|
||||
title="Filter by a job field"
|
||||
><i className="fa fa-filter" /></span>
|
||||
</span>
|
||||
|
||||
{/* Quick Filter Field */}
|
||||
<span
|
||||
id="quick-filter-parent"
|
||||
className="form-group form-inline"
|
||||
>
|
||||
<input
|
||||
id="quick-filter"
|
||||
className="form-control form-control-sm"
|
||||
required
|
||||
value={searchQueryStr}
|
||||
title="Click to enter filter values"
|
||||
onChange={evt => this.setSearchStr(evt)}
|
||||
onKeyDown={evt => this.search(evt)}
|
||||
type="text"
|
||||
placeholder="Filter platforms & jobs"
|
||||
/>
|
||||
<span
|
||||
id="quick-filter-clear-button"
|
||||
className="fa fa-times-circle"
|
||||
title="Clear this filter"
|
||||
onClick={this.clearFilterBox}
|
||||
/>
|
||||
</span>
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SecondaryNavBar.propTypes = {
|
||||
$injector: PropTypes.object.isRequired,
|
||||
updateButtonClick: PropTypes.func.isRequired,
|
||||
serverChanged: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
treeherder.component('secondaryNavBar', react2angular(
|
||||
SecondaryNavBar,
|
||||
['updateButtonClick', 'serverChanged'],
|
||||
['$injector']));
|
|
@ -0,0 +1,178 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { thEvents } from '../../js/constants';
|
||||
import TreeStatusModel from '../../models/treeStatus';
|
||||
import BugLinkify from '../../shared/BugLinkify';
|
||||
|
||||
const statusInfoMap = {
|
||||
open: {
|
||||
icon: 'fa-circle-o',
|
||||
color: 'tree-open',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
'approval required': {
|
||||
icon: 'fa-lock',
|
||||
color: 'tree-approval',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
closed: {
|
||||
icon: 'fa-times-circle',
|
||||
color: 'tree-closed',
|
||||
btnClass: 'btn-view-nav-closed',
|
||||
},
|
||||
unsupported: {
|
||||
icon: 'fa-question',
|
||||
color: 'tree-unavailable',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
'not retrieved yet': {
|
||||
icon: 'fa-spinner',
|
||||
pulseIcon: 'fa-pulse',
|
||||
color: 'tree-unavailable',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
error: {
|
||||
icon: 'fa-question',
|
||||
color: 'tree-unavailable',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
};
|
||||
|
||||
export default class WatchedRepo extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { $injector, watchedRepo } = this.props;
|
||||
this.$location = $injector.get('$location');
|
||||
this.thJobFilters = $injector.get('thJobFilters');
|
||||
this.$rootScope = $injector.get('$rootScope');
|
||||
this.ThRepositoryModel = $injector.get('ThRepositoryModel');
|
||||
|
||||
const pushLog = this.ThRepositoryModel.getRepo(watchedRepo) ?
|
||||
this.ThRepositoryModel.getRepo(watchedRepo).pushlogURL :
|
||||
'';
|
||||
|
||||
this.state = {
|
||||
status: 'not retrieved yet',
|
||||
reason: '',
|
||||
messageOfTheDay: '',
|
||||
statusInfo: {
|
||||
icon: 'fa-spinner',
|
||||
pulseIcon: 'fa-pulse',
|
||||
color: 'tree-unavailable',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
pushLog,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { watchedRepo } = this.props;
|
||||
|
||||
this.unlistenRepositoriesLoaded = this.$rootScope.$on(thEvents.repositoriesLoaded, () => {
|
||||
this.setState({ pushLog: this.ThRepositoryModel.getRepo(watchedRepo).pushlogURL });
|
||||
});
|
||||
|
||||
TreeStatusModel.get(watchedRepo).then((data) => {
|
||||
const treeStatus = data.result;
|
||||
|
||||
this.setState({
|
||||
status: treeStatus.status,
|
||||
reason: treeStatus.reason,
|
||||
messageOfTheDay: treeStatus.message_of_the_day,
|
||||
statusInfo: statusInfoMap[treeStatus.status],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unlistenRepositoriesLoaded();
|
||||
}
|
||||
|
||||
getRepoUrl() {
|
||||
const { repoName, watchedRepo } = this.props;
|
||||
const selectedJob = this.$location.search().selectedJob;
|
||||
const url = this.$location.absUrl().replace(`&selectedJob=${selectedJob}`, '');
|
||||
|
||||
return url.replace(`repo=${repoName}`, `repo=${watchedRepo}`);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { watchedRepo, repoName, unwatchRepo } = this.props;
|
||||
const { status, messageOfTheDay, reason, statusInfo, pushLog } = this.state;
|
||||
const activeClass = watchedRepo === repoName ? 'active' : '';
|
||||
const { btnClass, icon, color } = statusInfo;
|
||||
const pulseIcon = statusInfo.pulseIcon || '';
|
||||
const treeStatusName = TreeStatusModel.getTreeStatusName(watchedRepo);
|
||||
const changeRepoUrl = this.getRepoUrl();
|
||||
|
||||
return (
|
||||
<span className="btn-group">
|
||||
<a
|
||||
href={changeRepoUrl}
|
||||
className={`watched-repo-main-btn btn btn-sm ${btnClass} ${activeClass}`}
|
||||
type="button"
|
||||
title={status}
|
||||
>
|
||||
<i className={`fa ${icon} ${pulseIcon} ${color}`} /> {watchedRepo}
|
||||
</a>
|
||||
<button
|
||||
className={`watched-repo-info-btn btn btn-sm btn-view-nav ${activeClass}`}
|
||||
type="button"
|
||||
title={`${watchedRepo} info`}
|
||||
data-toggle="dropdown"
|
||||
><span className="fa fa-info-circle" /></button>
|
||||
{watchedRepo !== repoName && <button
|
||||
className={`watched-repo-unwatch-btn btn btn-sm btn-view-nav ${activeClass}`}
|
||||
onClick={() => unwatchRepo(watchedRepo)}
|
||||
title={`Unwatch ${watchedRepo}`}
|
||||
><span className="fa fa-times" /></button>}
|
||||
|
||||
<ul className="dropdown-menu" role="menu">
|
||||
{status === 'unsupported' && <React.Fragment>
|
||||
<li className="watched-repo-dropdown-item">
|
||||
<span>{watchedRepo} is not listed on <a
|
||||
href="https://mozilla-releng.net/treestatus"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Tree Status</a></span>
|
||||
</li>
|
||||
<li className="dropdown-divider" />
|
||||
</React.Fragment>}
|
||||
{!!reason && <li className="watched-repo-dropdown-item">
|
||||
<span><BugLinkify>{reason}</BugLinkify></span>
|
||||
</li>}
|
||||
{!!reason && !!messageOfTheDay && <li className="dropdown-divider" />}
|
||||
{!!messageOfTheDay && <li className="watched-repo-dropdown-item">
|
||||
<span><BugLinkify>{messageOfTheDay}</BugLinkify></span>
|
||||
</li>}
|
||||
{(!!reason || !!messageOfTheDay) && <li className="dropdown-divider" />}
|
||||
<li className="watched-repo-dropdown-item">
|
||||
<a
|
||||
href={`https://mozilla-releng.net/treestatus/show/${treeStatusName}`}
|
||||
className="dropdown-item"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Tree Status</a>
|
||||
</li>
|
||||
<li className="watched-repo-dropdown-item">
|
||||
<a
|
||||
href={pushLog}
|
||||
className="dropdown-item"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Pushlog</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WatchedRepo.propTypes = {
|
||||
$injector: PropTypes.object.isRequired,
|
||||
repoName: PropTypes.string.isRequired,
|
||||
watchedRepo: PropTypes.string.isRequired,
|
||||
unwatchRepo: PropTypes.func.isRequired,
|
||||
};
|
|
@ -179,24 +179,24 @@ export const thSimplePlatforms = [
|
|||
|
||||
export const thFailureResults = ['testfailed', 'busted', 'exception'];
|
||||
|
||||
export const thAllResultStates = [
|
||||
'success',
|
||||
export const thAllResultStatuses = [
|
||||
'testfailed',
|
||||
'busted',
|
||||
'exception',
|
||||
'success',
|
||||
'retry',
|
||||
'usercancel',
|
||||
'superseded',
|
||||
'running',
|
||||
'pending',
|
||||
'superseded',
|
||||
'runnable',
|
||||
];
|
||||
|
||||
export const thDefaultFilterResultStates = [
|
||||
'success',
|
||||
export const thDefaultFilterResultStatuses = [
|
||||
'testfailed',
|
||||
'busted',
|
||||
'exception',
|
||||
'success',
|
||||
'retry',
|
||||
'usercancel',
|
||||
'running',
|
||||
|
@ -308,6 +308,7 @@ export const thEvents = {
|
|||
autoclassifyOpenLogViewer: 'ac-open-log-viewer-EVT',
|
||||
selectRunnableJob: 'select-runnable-job-EVT',
|
||||
toggleFieldFilterVisible: 'toggle-field-filter-visible-EVT',
|
||||
repositoriesLoaded: 'repositories-loaded-EVT',
|
||||
};
|
||||
|
||||
export const phCompareDefaultOriginalRepo = 'mozilla-central';
|
||||
|
|
|
@ -1,137 +0,0 @@
|
|||
import treeherderApp from '../treeherder_app';
|
||||
import { thFailureResults, thAllResultStates, thEvents } from '../constants';
|
||||
|
||||
treeherderApp.controller('JobFilterCtrl', [
|
||||
'$scope', '$rootScope', 'thJobFilters',
|
||||
function JobFilterCtrl($scope, $rootScope, thJobFilters) {
|
||||
|
||||
$scope.filterOptions = thAllResultStates;
|
||||
|
||||
$scope.filterGroups = {
|
||||
failures: {
|
||||
value: 'failures',
|
||||
name: 'failures',
|
||||
resultStatuses: thFailureResults.slice(),
|
||||
},
|
||||
nonfailures: {
|
||||
value: 'nonfailures',
|
||||
name: 'non-failures',
|
||||
resultStatuses: ['success', 'retry', 'usercancel', 'superseded'],
|
||||
},
|
||||
'in progress': {
|
||||
value: 'in progress',
|
||||
name: 'in progress',
|
||||
resultStatuses: ['pending', 'running'],
|
||||
},
|
||||
};
|
||||
|
||||
$scope.resultStatusFilters = {};
|
||||
// flatten filter groups
|
||||
$scope.filterChicklets =
|
||||
['failures', $scope.filterGroups.nonfailures.resultStatuses, 'in progress'].reduce(
|
||||
(acc, val) => acc.concat(val), []);
|
||||
|
||||
/**
|
||||
* Handle toggling one of the individual result status filters in
|
||||
* the filter panel.
|
||||
*
|
||||
* @param filter
|
||||
*/
|
||||
$scope.toggleResultStatusFilter = function (filter) {
|
||||
if (!$scope.resultStatusFilters[filter]) {
|
||||
thJobFilters.removeFilter(thJobFilters.resultStatus, filter);
|
||||
} else {
|
||||
thJobFilters.addFilter(thJobFilters.resultStatus, filter);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isFilterOn = function (filter) {
|
||||
if (Object.keys($scope.filterGroups).indexOf(filter) !== -1) {
|
||||
return $scope.filterGroups[filter].resultStatuses.map(val =>
|
||||
$scope.resultStatusFilters[val]).some(val => val);
|
||||
}
|
||||
return $scope.resultStatusFilters[filter];
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle toggling one of the individual result status filter chicklets
|
||||
* on the nav bar
|
||||
*/
|
||||
$scope.toggleResultStatusFilterChicklet = function (filter) {
|
||||
let filterValues;
|
||||
if (Object.keys($scope.filterGroups).indexOf(filter) !== -1) {
|
||||
// this is a filter grouping, so toggle all on/off
|
||||
filterValues = $scope.filterGroups[filter].resultStatuses;
|
||||
} else {
|
||||
filterValues = [filter];
|
||||
}
|
||||
thJobFilters.toggleResultStatuses(filterValues);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the filters to show either unclassified or classified jobs,
|
||||
* neither or both.
|
||||
*/
|
||||
$scope.toggleClassifiedFilter = function () {
|
||||
const func = $scope.classifiedFilter ? thJobFilters.removeFilter : thJobFilters.addFilter;
|
||||
func(thJobFilters.classifiedState, 'classified');
|
||||
};
|
||||
|
||||
$scope.toggleUnClassifiedFilter = function () {
|
||||
const func = $scope.unClassifiedFilter ? thJobFilters.removeFilter : thJobFilters.addFilter;
|
||||
func(thJobFilters.classifiedState, 'unclassified');
|
||||
};
|
||||
|
||||
$scope.thJobFilters = thJobFilters;
|
||||
|
||||
const updateToggleFilters = function () {
|
||||
for (let i = 0; i < $scope.filterOptions.length; i++) {
|
||||
const opt = $scope.filterOptions[i];
|
||||
$scope.resultStatusFilters[opt] = thJobFilters.getResultStatusArray().indexOf(opt) !== -1;
|
||||
}
|
||||
|
||||
// whether or not to show classified jobs
|
||||
// these are a special case of filtering because we're not checking
|
||||
// for a value, just whether the job has any value set or not.
|
||||
// just a boolean check either way
|
||||
const classifiedState = thJobFilters.getClassifiedStateArray();
|
||||
$scope.classifiedFilter = classifiedState.indexOf('classified') !== -1;
|
||||
$scope.unClassifiedFilter = classifiedState.indexOf('unclassified') !== -1;
|
||||
};
|
||||
|
||||
updateToggleFilters();
|
||||
|
||||
$rootScope.$on(thEvents.globalFilterChanged, function () {
|
||||
updateToggleFilters();
|
||||
});
|
||||
},
|
||||
]);
|
||||
|
||||
treeherderApp.controller('SearchCtrl', [
|
||||
'$scope', '$rootScope', 'thJobFilters',
|
||||
function SearchCtrl(
|
||||
$scope, $rootScope, thJobFilters) {
|
||||
|
||||
const getSearchStr = function () {
|
||||
const ss = thJobFilters.getFieldFiltersObj().searchStr;
|
||||
if (ss) {
|
||||
return ss.join(' ');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
$scope.searchQueryStr = getSearchStr();
|
||||
$rootScope.$on(thEvents.globalFilterChanged, function () {
|
||||
$scope.searchQueryStr = getSearchStr();
|
||||
});
|
||||
|
||||
$scope.search = function (ev) {
|
||||
// User hit enter
|
||||
if (ev.keyCode === 13) {
|
||||
const filterVal = $scope.searchQueryStr === '' ? null : $scope.searchQueryStr;
|
||||
thJobFilters.replaceFilter('searchStr', filterVal);
|
||||
$rootScope.$broadcast('blur-this', 'quick-filter');
|
||||
}
|
||||
};
|
||||
},
|
||||
]);
|
|
@ -3,20 +3,20 @@ import _ from 'lodash';
|
|||
import Mousetrap from 'mousetrap';
|
||||
|
||||
import treeherderApp from '../treeherder_app';
|
||||
import { thTitleSuffixLimit, thDefaultRepo, thJobNavSelectors, thEvents } from '../constants';
|
||||
import {
|
||||
thTitleSuffixLimit, thDefaultRepo, thJobNavSelectors, thEvents, thAllResultStatuses,
|
||||
} from '../constants';
|
||||
|
||||
treeherderApp.controller('MainCtrl', [
|
||||
'$scope', '$rootScope', '$location', '$timeout',
|
||||
'ThRepositoryModel', '$document',
|
||||
'thClassificationTypes', '$window',
|
||||
'thJobFilters', 'ThResultSetStore', 'thNotify',
|
||||
'$httpParamSerializer',
|
||||
function MainController(
|
||||
$scope, $rootScope, $location, $timeout,
|
||||
ThRepositoryModel, $document,
|
||||
thClassificationTypes, $window,
|
||||
thJobFilters, ThResultSetStore, thNotify,
|
||||
$httpParamSerializer) {
|
||||
thJobFilters, ThResultSetStore, thNotify) {
|
||||
|
||||
if (window.navigator.userAgent.indexOf('Firefox/52') !== -1) {
|
||||
thNotify.send('Firefox ESR52 is not supported. Please update to ESR60 or ideally release/beta/nightly.',
|
||||
|
@ -40,10 +40,10 @@ treeherderApp.controller('MainCtrl', [
|
|||
$rootScope.revision = $location.search().revision;
|
||||
thClassificationTypes.load();
|
||||
|
||||
// TODO: remove this once we're off of Angular completely.
|
||||
// TODO: remove this when the thGlobalTopNavPanel is converted to React.
|
||||
$rootScope.countPinnedJobs = () => 0;
|
||||
|
||||
// TODO: remove this when we convert the watchedRepoNavBar to React.
|
||||
// TODO: remove this when SecondaryNavBar has JobView as an ancestor.
|
||||
$scope.updateButtonClick = function () {
|
||||
if (window.confirm('Reload the page to pick up Treeherder updates?')) {
|
||||
window.location.reload(true);
|
||||
|
@ -92,7 +92,7 @@ treeherderApp.controller('MainCtrl', [
|
|||
};
|
||||
|
||||
$rootScope.getWindowTitle = function () {
|
||||
const ufc = $scope.getAllUnclassifiedFailureCount();
|
||||
const ufc = ThResultSetStore.getAllUnclassifiedFailureCount();
|
||||
const params = $location.search();
|
||||
|
||||
// repoName is undefined for the first few title update attempts, show something sensible
|
||||
|
@ -108,45 +108,6 @@ treeherderApp.controller('MainCtrl', [
|
|||
return title;
|
||||
};
|
||||
|
||||
$scope.repoModel = ThRepositoryModel;
|
||||
|
||||
/**
|
||||
* The watched repos in the nav bar can be either on the left or the
|
||||
* right side of the screen and the drop-down menu may get cut off
|
||||
* if it pulls right while on the left side of the screen.
|
||||
* And it can change any time the user re-sizes the window, so we must
|
||||
* check this each time a drop-down is invoked.
|
||||
*/
|
||||
$scope.setDropDownPull = function (event) {
|
||||
const element = event.target.offsetParent;
|
||||
if (element.offsetLeft > $(window).width() / 2) {
|
||||
$(element).find('.dropdown-menu').addClass('pull-right');
|
||||
} else {
|
||||
$(element).find('.dropdown-menu').removeClass('pull-right');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
$scope.getFilteredUnclassifiedFailureCount = ThResultSetStore.getFilteredUnclassifiedFailureCount;
|
||||
$scope.getAllUnclassifiedFailureCount = ThResultSetStore.getAllUnclassifiedFailureCount;
|
||||
|
||||
$scope.toggleUnclassifiedFailures = thJobFilters.toggleUnclassifiedFailures;
|
||||
|
||||
$scope.toggleInProgress = function () {
|
||||
thJobFilters.toggleInProgress();
|
||||
};
|
||||
|
||||
$scope.getGroupState = function () {
|
||||
return $location.search().group_state || 'collapsed';
|
||||
};
|
||||
|
||||
$scope.groupState = $scope.getGroupState();
|
||||
|
||||
$scope.toggleGroupState = function () {
|
||||
const newGroupState = $scope.groupState === 'collapsed' ? 'expanded' : null;
|
||||
$location.search('group_state', newGroupState);
|
||||
};
|
||||
|
||||
/*
|
||||
* This updates which tier checkboxes are set according to the filters.
|
||||
* It's made slightly tricky due to the fact that, if you remove all
|
||||
|
@ -194,6 +155,14 @@ treeherderApp.controller('MainCtrl', [
|
|||
}
|
||||
};
|
||||
|
||||
// For the Filter menu on the thGlobalTopNavPanel
|
||||
$scope.resultStatuses = thAllResultStatuses.slice();
|
||||
$scope.resultStatuses.splice(thAllResultStatuses.indexOf('runnable'), 1);
|
||||
|
||||
$scope.isFilterOn = field => (
|
||||
[...thJobFilters.getResultStatusArray(), ...thJobFilters.getClassifiedStateArray()].includes(field)
|
||||
);
|
||||
|
||||
// Setup key event handling
|
||||
const stopOverrides = new Map();
|
||||
|
||||
|
@ -293,12 +262,12 @@ treeherderApp.controller('MainCtrl', [
|
|||
['ctrl+shift+f', (ev) => {
|
||||
// Prevent shortcut key overflow during focus
|
||||
ev.preventDefault();
|
||||
$scope.$evalAsync($scope.clearFilterBox());
|
||||
$scope.$evalAsync(thJobFilters.removeFilter('searchStr'));
|
||||
}],
|
||||
|
||||
// Shortcut: toggle display in-progress jobs (pending/running)
|
||||
['i', () => {
|
||||
$scope.$evalAsync($scope.toggleInProgress());
|
||||
$scope.$evalAsync(thJobFilters.toggleInProgress());
|
||||
}],
|
||||
|
||||
// Shortcut: ignore selected in the autoclasify panel
|
||||
|
@ -368,7 +337,7 @@ treeherderApp.controller('MainCtrl', [
|
|||
|
||||
// Shortcut: display only unclassified failures
|
||||
['u', () => {
|
||||
$scope.$evalAsync($scope.toggleUnclassifiedFailures);
|
||||
$scope.$evalAsync(thJobFilters.toggleUnclassifiedFailures);
|
||||
}],
|
||||
|
||||
// Shortcut: clear the pinboard
|
||||
|
@ -469,22 +438,6 @@ treeherderApp.controller('MainCtrl', [
|
|||
(acc, prop) => (locationSearch[prop] ? { ...acc, [prop]: locationSearch[prop] } : acc), {});
|
||||
};
|
||||
|
||||
$scope.toggleFieldFilterVisible = function () {
|
||||
$rootScope.$emit(thEvents.toggleFieldFilterVisible);
|
||||
};
|
||||
|
||||
$scope.fromChangeValue = function () {
|
||||
let url = window.location.href;
|
||||
url = url.replace('&fromchange=' + $location.search().fromchange, '');
|
||||
return url;
|
||||
};
|
||||
|
||||
$scope.toChangeValue = function () {
|
||||
let url = window.location.href;
|
||||
url = url.replace('&tochange=' + $location.search().tochange, '');
|
||||
return url;
|
||||
};
|
||||
|
||||
$scope.cachedReloadTriggerParams = getNewReloadTriggerParams();
|
||||
|
||||
// reload the page if certain params were changed in the URL. For
|
||||
|
@ -518,49 +471,14 @@ treeherderApp.controller('MainCtrl', [
|
|||
}
|
||||
$rootScope.skipNextPageReload = false;
|
||||
|
||||
// handle a change in the groupState whether it was by the button
|
||||
// or directly in the url.
|
||||
const newGroupState = $scope.getGroupState();
|
||||
if (newGroupState !== $scope.groupState) {
|
||||
$scope.groupState = newGroupState;
|
||||
$rootScope.$emit(thEvents.groupStateChanged, newGroupState);
|
||||
}
|
||||
|
||||
// handle a change in the show duplicate jobs variable
|
||||
// whether it was by the button or directly in the url.
|
||||
const showDuplicateJobs = $scope.isShowDuplicateJobs();
|
||||
if (showDuplicateJobs !== $scope.showDuplicateJobs) {
|
||||
$scope.showDuplicateJobs = showDuplicateJobs;
|
||||
$rootScope.$emit(thEvents.duplicateJobsVisibilityChanged);
|
||||
}
|
||||
|
||||
// update the tier drop-down menu if a tier setting was changed
|
||||
$scope.updateTiers();
|
||||
});
|
||||
|
||||
$scope.changeRepo = function (repo_name) {
|
||||
// preserves filter params as the user changes repos and revisions
|
||||
$location.search(_.extend({
|
||||
repo: repo_name,
|
||||
}, thJobFilters.getActiveFilters()));
|
||||
};
|
||||
|
||||
$scope.filterParams = function () {
|
||||
let filters = $httpParamSerializer(thJobFilters.getActiveFilters());
|
||||
if (filters) {
|
||||
filters = '&' + filters;
|
||||
}
|
||||
return filters;
|
||||
};
|
||||
|
||||
$scope.pinJobs = function () {
|
||||
$rootScope.$emit(thEvents.pinJobs, ThResultSetStore.getAllShownJobs());
|
||||
};
|
||||
|
||||
$scope.clearFilterBox = function () {
|
||||
thJobFilters.removeFilter('searchStr');
|
||||
};
|
||||
|
||||
$scope.onscreenOverlayShowing = false;
|
||||
|
||||
$scope.onscreenShortcutsShowing = false;
|
||||
|
@ -570,16 +488,5 @@ treeherderApp.controller('MainCtrl', [
|
|||
};
|
||||
|
||||
$scope.jobFilters = thJobFilters;
|
||||
|
||||
$scope.isShowDuplicateJobs = function () {
|
||||
return $location.search().duplicate_jobs === 'visible';
|
||||
};
|
||||
$scope.showDuplicateJobs = $scope.isShowDuplicateJobs();
|
||||
$scope.toggleShowDuplicateJobs = function () {
|
||||
const showDuplicateJobs = !$scope.showDuplicateJobs;
|
||||
|
||||
// $scope.showDuplicateJobs will be changed in watch function above
|
||||
$location.search('duplicate_jobs', showDuplicateJobs ? 'visible' : null);
|
||||
};
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -1,114 +1,9 @@
|
|||
import $ from 'jquery';
|
||||
|
||||
import treeherder from '../../treeherder';
|
||||
import thWatchedRepoTemplate from '../../../partials/main/thWatchedRepo.html';
|
||||
import thWatchedRepoInfoDropDownTemplate from '../../../partials/main/thWatchedRepoInfoDropDown.html';
|
||||
import thRepoMenuItemTemplate from '../../../partials/main/thRepoMenuItem.html';
|
||||
import thResultStatusChickletTemplate from '../../../partials/main/thResultStatusChicklet.html';
|
||||
import { getBtnClass } from '../../../helpers/job';
|
||||
import TreeStatusModel from '../../../models/treeStatus';
|
||||
|
||||
treeherder.directive('thWatchedRepo', [
|
||||
'ThRepositoryModel',
|
||||
function (ThRepositoryModel) {
|
||||
|
||||
const statusInfo = {
|
||||
open: {
|
||||
icon: 'fa-circle-o',
|
||||
color: 'tree-open',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
'approval required': {
|
||||
icon: 'fa-lock',
|
||||
color: 'tree-approval',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
closed: {
|
||||
icon: 'fa-times-circle',
|
||||
color: 'tree-closed',
|
||||
btnClass: 'btn-view-nav-closed',
|
||||
},
|
||||
unsupported: {
|
||||
icon: 'fa-question',
|
||||
color: 'tree-unavailable',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
'not retrieved yet': {
|
||||
icon: 'fa-spinner',
|
||||
iconClass: 'fa-pulse',
|
||||
color: 'tree-unavailable',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
error: {
|
||||
icon: 'fa-question',
|
||||
color: 'tree-unavailable',
|
||||
btnClass: 'btn-view-nav',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function (scope) {
|
||||
|
||||
scope.repoData = ThRepositoryModel.repos[scope.watchedRepo];
|
||||
|
||||
scope.updateTitleText = function () {
|
||||
if (scope.repoData.treeStatus) {
|
||||
scope.titleText = scope.repoData.treeStatus.status;
|
||||
if (scope.repoData.treeStatus.reason) {
|
||||
scope.titleText = scope.titleText + ' - ' +
|
||||
scope.repoData.treeStatus.reason;
|
||||
}
|
||||
if (scope.repoData.treeStatus.message_of_the_day) {
|
||||
scope.titleText = scope.titleText + ' - ' +
|
||||
scope.repoData.treeStatus.message_of_the_day;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
scope.btnClass = 'btn-view-nav';
|
||||
|
||||
scope.$watch('repoData.treeStatus.status', function (newVal) {
|
||||
if (newVal) {
|
||||
const si = statusInfo[newVal];
|
||||
scope.statusIcon = si.icon;
|
||||
scope.statusIconClass = si.iconClass || '';
|
||||
scope.statusColor = si.color;
|
||||
scope.btnClass = si.btnClass;
|
||||
scope.updateTitleText();
|
||||
}
|
||||
});
|
||||
},
|
||||
template: thWatchedRepoTemplate,
|
||||
};
|
||||
}]);
|
||||
|
||||
treeherder.directive('thWatchedRepoInfoDropDown', [
|
||||
'ThRepositoryModel',
|
||||
function (ThRepositoryModel) {
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
link: function (scope, element, attrs) {
|
||||
scope.name = attrs.name;
|
||||
scope.treeStatus = TreeStatusModel.getTreeStatusName(attrs.name);
|
||||
const repo_obj = ThRepositoryModel.getRepo(attrs.name);
|
||||
scope.pushlog = repo_obj.pushlogURL;
|
||||
scope.$watch('repoData.treeStatus', function (newVal) {
|
||||
if (newVal) {
|
||||
scope.reason = newVal.reason;
|
||||
scope.message_of_the_day = newVal.message_of_the_day;
|
||||
}
|
||||
}, true);
|
||||
},
|
||||
template: thWatchedRepoInfoDropDownTemplate,
|
||||
};
|
||||
}]);
|
||||
|
||||
|
||||
treeherder.directive('thCheckboxDropdownContainer', function () {
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element) {
|
||||
|
@ -137,7 +32,6 @@ treeherder.directive('thCheckboxDropdownContainer', function () {
|
|||
|
||||
treeherder.directive('thRepoMenuItem',
|
||||
function () {
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
|
@ -152,13 +46,3 @@ treeherder.directive('thRepoMenuItem',
|
|||
template: thRepoMenuItemTemplate,
|
||||
};
|
||||
});
|
||||
|
||||
treeherder.directive('thResultStatusChicklet', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function (scope) {
|
||||
scope.chickletClass = `${getBtnClass(scope.filterName)}-filter-chicklet`;
|
||||
},
|
||||
template: thResultStatusChickletTemplate,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -2,27 +2,16 @@ import numeral from 'numeral';
|
|||
|
||||
import treeherder from './treeherder';
|
||||
|
||||
treeherder.filter('stripHtml', function () {
|
||||
return function (input) {
|
||||
const str = input || '';
|
||||
return str.replace(/<\/?[^>]+>/gi, '');
|
||||
};
|
||||
});
|
||||
|
||||
treeherder.filter('linkifyBugs', function () {
|
||||
return function (input) {
|
||||
let str = input || '';
|
||||
|
||||
const bug_matches = str.match(/-- ([0-9]+)|bug.([0-9]+)/ig);
|
||||
const pr_matches = str.match(/PR#([0-9]+)/ig);
|
||||
|
||||
// Settings
|
||||
const bug_title = 'bugzilla.mozilla.org';
|
||||
const bug_url = '<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=$1" ' +
|
||||
'data-bugid="$1" title="' + bug_title + '">$1</a>';
|
||||
const pr_title = 'github.com';
|
||||
const pr_url = '<a href="https://github.com/mozilla-b2g/gaia/pull/$1" ' +
|
||||
'data-prid="$1" title="' + pr_title + '">$1</a>';
|
||||
|
||||
if (bug_matches) {
|
||||
// Separate passes to preserve prefix
|
||||
|
@ -31,12 +20,6 @@ treeherder.filter('linkifyBugs', function () {
|
|||
str = str.replace(/-- ([0-9]+)/g, '-- ' + bug_url);
|
||||
}
|
||||
|
||||
if (pr_matches) {
|
||||
// Separate passes to preserve prefix
|
||||
str = str.replace(/PR#([0-9]+)/g, 'PR#' + pr_url);
|
||||
str = str.replace(/pr#([0-9]+)/g, 'pr#' + pr_url);
|
||||
}
|
||||
|
||||
return str;
|
||||
};
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import _ from 'lodash';
|
|||
|
||||
import treeherder from '../treeherder';
|
||||
import { getApiUrl } from '../../helpers/url';
|
||||
import { thRepoGroupOrder } from '../constants';
|
||||
import { thRepoGroupOrder, thEvents } from '../constants';
|
||||
import TreeStatusModel from '../../models/treeStatus';
|
||||
|
||||
treeherder.factory('ThRepositoryModel', [
|
||||
|
@ -243,6 +243,7 @@ treeherder.factory('ThRepositoryModel', [
|
|||
}
|
||||
saveWatchedRepos();
|
||||
}
|
||||
$rootScope.$emit(thEvents.repositoriesLoaded);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import difference from 'lodash/difference';
|
|||
|
||||
import treeherder from '../treeherder';
|
||||
import { getStatus } from '../../helpers/job';
|
||||
import { thFailureResults, thDefaultFilterResultStates, thEvents } from '../constants';
|
||||
import { thFailureResults, thDefaultFilterResultStatuses, thEvents } from '../constants';
|
||||
|
||||
/**
|
||||
This service handles whether or not a job, job group or platform row should
|
||||
|
@ -51,7 +51,7 @@ treeherder.factory('thJobFilters', [
|
|||
|
||||
// default filter values, when a filter is not specified in the query string
|
||||
const DEFAULTS = {
|
||||
resultStatus: thDefaultFilterResultStates,
|
||||
resultStatus: thDefaultFilterResultStatuses,
|
||||
classifiedState: ['classified', 'unclassified'],
|
||||
tier: ['1', '2'],
|
||||
};
|
||||
|
@ -123,6 +123,12 @@ treeherder.factory('thJobFilters', [
|
|||
},
|
||||
};
|
||||
|
||||
const FILTER_GROUPS = {
|
||||
failures: thFailureResults.slice(),
|
||||
nonfailures: ['success', 'retry', 'usercancel', 'superseded'],
|
||||
'in progress': ['pending', 'running'],
|
||||
};
|
||||
|
||||
// filter caches so that we only collect them when the filter params
|
||||
// change in the query string
|
||||
let cachedResultStatusFilters = {};
|
||||
|
@ -288,9 +294,7 @@ treeherder.factory('thJobFilters', [
|
|||
if (_matchesDefaults(field, newQsVal)) {
|
||||
newQsVal = null;
|
||||
}
|
||||
$timeout(() => {
|
||||
$location.search(_withPrefix(field), newQsVal);
|
||||
}, 0);
|
||||
$timeout(() => $location.search(_withPrefix(field), newQsVal));
|
||||
}
|
||||
|
||||
function removeFilter(field, value) {
|
||||
|
@ -306,8 +310,7 @@ treeherder.factory('thJobFilters', [
|
|||
newQsVal = null;
|
||||
}
|
||||
}
|
||||
$location.search(_withPrefix(field), newQsVal);
|
||||
$rootScope.$apply();
|
||||
$timeout(() => $location.search(_withPrefix(field), newQsVal));
|
||||
}
|
||||
|
||||
function replaceFilter(field, value) {
|
||||
|
@ -319,8 +322,7 @@ treeherder.factory('thJobFilters', [
|
|||
const locationSearch = $location.search();
|
||||
_stripFieldFilters(locationSearch);
|
||||
_stripClearableFieldFilters(locationSearch);
|
||||
$location.search(locationSearch);
|
||||
$rootScope.$apply();
|
||||
$timeout(() => $location.search(locationSearch));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -368,6 +370,11 @@ treeherder.factory('thJobFilters', [
|
|||
$location.search(QS_RESULT_STATUS, rsValues);
|
||||
}
|
||||
|
||||
function toggleClassifiedFilter(classifiedState) {
|
||||
const func = getClassifiedStateArray().includes(classifiedState) ? removeFilter : addFilter;
|
||||
func('classifiedState', classifiedState);
|
||||
}
|
||||
|
||||
function toggleUnclassifiedFailures() {
|
||||
if (_isUnclassifiedFailures()) {
|
||||
resetNonFieldFilters();
|
||||
|
@ -587,6 +594,7 @@ treeherder.factory('thJobFilters', [
|
|||
toggleResultStatuses: toggleResultStatuses,
|
||||
toggleInProgress: toggleInProgress,
|
||||
toggleUnclassifiedFailures: toggleUnclassifiedFailures,
|
||||
toggleClassifiedFilter: toggleClassifiedFilter,
|
||||
setOnlySuperseded: setOnlySuperseded,
|
||||
getActiveFilters: getActiveFilters,
|
||||
|
||||
|
@ -601,6 +609,7 @@ treeherder.factory('thJobFilters', [
|
|||
getFieldChoices: getFieldChoices,
|
||||
|
||||
// CONSTANTS
|
||||
filterGroups: FILTER_GROUPS,
|
||||
classifiedState: CLASSIFIED_STATE,
|
||||
resultStatus: RESULT_STATUS,
|
||||
tiers: TIERS,
|
||||
|
|
|
@ -4,12 +4,10 @@ import ngRoute from 'angular-route';
|
|||
import uiBootstrap from 'angular1-ui-bootstrap4';
|
||||
|
||||
import treeherderModule from './treeherder';
|
||||
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 thShortcutTableTemplate from '../partials/main/thShortcutTable.html';
|
||||
import thWatchedRepoNavPanelTemplate from '../partials/main/thWatchedRepoNavPanel.html';
|
||||
|
||||
const treeherderApp = angular.module('treeherder.app', [
|
||||
treeherderModule.name,
|
||||
|
@ -51,7 +49,7 @@ treeherderApp.config(['$compileProvider', '$locationProvider', '$routeProvider',
|
|||
|
||||
$routeProvider
|
||||
.when('/jobs', {
|
||||
// see controllers/filters.js ``skipNextSearchChangeReload`` for
|
||||
// see controllers/main.js ``skipNextPageReload`` for
|
||||
// why we set this to false.
|
||||
reloadOnSearch: false,
|
||||
})
|
||||
|
@ -62,12 +60,10 @@ treeherderApp.config(['$compileProvider', '$locationProvider', '$routeProvider',
|
|||
}]).run(['$templateCache', ($templateCache) => {
|
||||
// Templates used by ng-include have to be manually put in the template cache.
|
||||
// Those used by directives should instead be imported at point of use.
|
||||
$templateCache.put('partials/main/thFilterChicklets.html', thFilterChickletsTemplate);
|
||||
$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/thShortcutTable.html', thShortcutTableTemplate);
|
||||
$templateCache.put('partials/main/thWatchedRepoNavPanel.html', thWatchedRepoNavPanelTemplate);
|
||||
}]);
|
||||
|
||||
export default treeherderApp;
|
||||
|
|
|
@ -18,7 +18,7 @@ export default class TreeStatusModel {
|
|||
return Promise.resolve({
|
||||
result: {
|
||||
status: 'unsupported',
|
||||
message_of_the_day: `${repoName} is not listed on <a href="https://mozilla-releng.net/treestatus">TreeStatus</a>`,
|
||||
message_of_the_day: '',
|
||||
reason: '',
|
||||
tree: repoName,
|
||||
},
|
||||
|
@ -30,7 +30,7 @@ export default class TreeStatusModel {
|
|||
Promise.resolve({
|
||||
result: {
|
||||
status: 'error',
|
||||
message_of_the_day: 'Unable to connect to the <a href="https://mozilla-releng.net/treestatus">TreeStatus</a> API',
|
||||
message_of_the_day: 'Unable to connect to the https://mozilla-releng.net/treestatus API',
|
||||
reason,
|
||||
tree: repoName,
|
||||
},
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
<span id="filter-chicklets" ng-controller="JobFilterCtrl">
|
||||
<!-- result status filters -->
|
||||
<span ng-repeat="filterName in filterChicklets">
|
||||
<th-result-status-chicklet></th-result-status-chicklet>
|
||||
</span>
|
||||
</span>
|
|
@ -109,7 +109,7 @@
|
|||
</span>
|
||||
|
||||
<!-- Filters Menu -->
|
||||
<span ng-controller="JobFilterCtrl" >
|
||||
<span>
|
||||
<span th-checkbox-dropdown-container class="dropdown">
|
||||
<button id="filterLabel" title="Set filters" role="button"
|
||||
data-toggle="dropdown"
|
||||
|
@ -118,48 +118,42 @@
|
|||
<ul id="filter-dropdown"
|
||||
class="dropdown-menu nav-dropdown-menu-right checkbox-dropdown-menu"
|
||||
role="menu" aria-labelledby="filterLabel">
|
||||
<span class="filtergroup" ng-repeat="(group_order, group) in filterGroups">
|
||||
<li role="presentation" class="dropdown-header">
|
||||
{{::group.name}}
|
||||
<span class="fa fa-info-circle"></span>
|
||||
</li>
|
||||
<span ng-repeat="filterName in group.resultStatuses" >
|
||||
<label class="{{::checkClass}} dropdown-item">
|
||||
<input type="checkbox"
|
||||
class="mousetrap"
|
||||
id="{{::filterName}}"
|
||||
ng-model="resultStatusFilters[filterName]"
|
||||
ng-change="toggleResultStatusFilter(filterName)"> {{::filterName}}
|
||||
</label>
|
||||
</span>
|
||||
<li class="dropdown-divider separator"></li>
|
||||
</span>
|
||||
|
||||
|
||||
<li>
|
||||
<span ng-repeat="filterName in resultStatuses">
|
||||
<span>
|
||||
<label class="{{::checkClass}} dropdown-item">
|
||||
<input type="checkbox"
|
||||
class="mousetrap"
|
||||
id="{{::filterName}}"
|
||||
ng-checked="isFilterOn(filterName)"
|
||||
ng-click="jobFilters.toggleResultStatuses([filterName])"> {{::filterName}}
|
||||
</label>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
<li class="dropdown-divider separator"></li>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox"
|
||||
id="classified"
|
||||
ng-model="classifiedFilter"
|
||||
ng-click="toggleClassifiedFilter()"> classified
|
||||
ng-checked="isFilterOn('classified')"
|
||||
ng-click="jobFilters.toggleClassifiedFilter('classified')"> classified
|
||||
</label>
|
||||
<label class="dropdown-item">
|
||||
<input type="checkbox"
|
||||
id="unclassified"
|
||||
ng-model="unClassifiedFilter"
|
||||
ng-click="toggleUnClassifiedFilter()"> un-classified
|
||||
ng-checked="isFilterOn('unclassified')"
|
||||
ng-click="jobFilters.toggleClassifiedFilter('unclassified')"> un-classified
|
||||
</label>
|
||||
|
||||
<li class="dropdown-divider separator"></li>
|
||||
|
||||
<li title="Pin all jobs that pass the global filters"
|
||||
class="dropdown-item"
|
||||
ng-click="pinJobs()">Pin all showing</li>
|
||||
<li title="Show only superseded jobs"
|
||||
class="dropdown-item"
|
||||
ng-click="thJobFilters.setOnlySuperseded()">Superseded only</li>
|
||||
ng-click="jobFilters.setOnlySuperseded()">Superseded only</li>
|
||||
<li title="Reset to default status filters"
|
||||
class="dropdown-item"
|
||||
ng-click="thJobFilters.resetNonFieldFilters()">Reset</li>
|
||||
ng-click="jobFilters.resetNonFieldFilters()">Reset</li>
|
||||
</ul>
|
||||
</span>
|
||||
</span>
|
||||
|
@ -181,5 +175,5 @@
|
|||
</span>
|
||||
|
||||
</div>
|
||||
<ng-include class="watched-repo-navbar" src="'partials/main/thWatchedRepoNavPanel.html'" ng-show="locationPath==='jobs'"></ng-include>
|
||||
<secondary-nav-bar update-button-click="updateButtonClick" server-changed="serverChanged" />
|
||||
</nav>
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<span class="btn btn-view-nav btn-sm btn-nav-filter {{chickletClass}} fa"
|
||||
ng-class="{'fa-dot-circle-o': (isFilterOn(filterName)), 'fa-circle-thin': (!isFilterOn(filterName))}"
|
||||
ng-click="toggleResultStatusFilterChicklet(filterName)"
|
||||
title="{{filterName}}">
|
||||
</span>
|
|
@ -1,28 +0,0 @@
|
|||
<span class="btn-group">
|
||||
<button class="watched-repo-main-btn btn btn-sm {{btnClass}}"
|
||||
ng-class="{'active': watchedRepo===repoName}"
|
||||
ng-click="changeRepo(watchedRepo)"
|
||||
type="button"
|
||||
title="{{titleText|stripHtml}}">
|
||||
<i class="fa {{statusIcon}} {{statusIconClass}} {{statusColor}}"></i> {{::watchedRepo}}
|
||||
</button>
|
||||
<button class="watched-repo-info-btn btn btn-sm {{btnClass}}"
|
||||
ng-class="{'active': watchedRepo===repoName}"
|
||||
ng-click="setDropDownPull($event)"
|
||||
type="button"
|
||||
title="{{::watchedRepo}} info"
|
||||
data-toggle="dropdown">
|
||||
<span class="fa fa-info-circle"></span>
|
||||
</button>
|
||||
<button class="watched-repo-unwatch-btn btn btn-sm {{btnClass}}"
|
||||
ng-class="{'active': watchedRepo===repoName}"
|
||||
ng-click="repoModel.unwatchRepo(watchedRepo)"
|
||||
ng-hide="watchedRepo===repoName"
|
||||
title="Unwatch {{::watchedRepo}}">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>
|
||||
<th-watched-repo-info-drop-down name="{{::watchedRepo}}"
|
||||
reason="{{repos[watchedRepo].treeStatus.reason}}"
|
||||
message_of_the_day="{{repos[watchedRepo].treeStatus.message_of_the_day}}">
|
||||
</th-watched-repo-info-drop-down>
|
||||
</span>
|
|
@ -1,18 +0,0 @@
|
|||
<ul class="dropdown-menu" role="menu">
|
||||
<li ng-show="reason" class="watched-repo-dropdown-item">
|
||||
<span ng-bind-html="reason|linkifyBugs"></span>
|
||||
</li>
|
||||
<li class="dropdown-divider" ng-show="reason && message_of_the_day"></li>
|
||||
<li ng-show="message_of_the_day" class="watched-repo-dropdown-item">
|
||||
<span ng-bind-html="message_of_the_day"></span>
|
||||
</li>
|
||||
<li class="dropdown-divider" ng-show="reason || message_of_the_day"></li>
|
||||
<li class="watched-repo-dropdown-item">
|
||||
<a href="https://mozilla-releng.net/treestatus/show/{{::treeStatus}}"
|
||||
class="dropdown-item"
|
||||
target="_blank" rel="noopener">Tree Status</a>
|
||||
</li>
|
||||
<li class="watched-repo-dropdown-item">
|
||||
<a href="{{::pushlog}}" class="dropdown-item" target="_blank" rel="noopener">Pushlog</a>
|
||||
</li>
|
||||
</ul>
|
|
@ -1,83 +0,0 @@
|
|||
<div id="watched-repo-navbar" class="th-context-navbar navbar-dark watched-repo-navbar">
|
||||
<span class="justify-content-between w-100 d-flex flex-wrap">
|
||||
<span class="d-flex"><th-watched-repo ng-repeat="watchedRepo in repoModel.watchedRepos" /></span>
|
||||
<form role="search" class="form-inline flex-row">
|
||||
|
||||
<span class="btn btn-sm btn-view-nav nav-menu-btn"
|
||||
ng-show="serverChanged" ng-cloak
|
||||
ng-click="updateButtonClick()"
|
||||
id="revisionChangedLabel"
|
||||
title="New version of Treeherder has been deployed. Reload to pick up changes.">
|
||||
<span class="fa fa-exclamation-circle"></span> Treeherder update available
|
||||
</span>
|
||||
|
||||
<!--Unclassified Failures Button-->
|
||||
<span class="btn btn-sm"
|
||||
title="Loaded failures / toggle filtering for unclassified failures"
|
||||
tabindex="0" role="button"
|
||||
ng-class="{'btn-unclassified-failures': getAllUnclassifiedFailureCount(),
|
||||
'btn-view-nav': getAllUnclassifiedFailureCount()===0}"
|
||||
ng-click="toggleUnclassifiedFailures()">
|
||||
<span id="unclassified-failure-count">
|
||||
{{ getAllUnclassifiedFailureCount() }}</span> unclassified
|
||||
</span>
|
||||
|
||||
<!--Filtered Unclassified Failures Button-->
|
||||
<span class="navbar-badge badge badge-secondary badge-pill"
|
||||
title="Reflects the unclassified failures which pass the current filters"
|
||||
ng-hide="getFilteredUnclassifiedFailureCount() === getAllUnclassifiedFailureCount()">
|
||||
<span id="filtered-unclassified-failure-count">
|
||||
{{ getFilteredUnclassifiedFailureCount() }}</span>
|
||||
</span>
|
||||
|
||||
<!-- Toggle Duplicate Jobs -->
|
||||
<span class="btn btn-view-nav btn-sm btn-toggle-duplicate-jobs"
|
||||
tabindex="1" role="button"
|
||||
title="{{ showDuplicateJobs ? 'Hide duplicate jobs' : 'Show duplicate jobs' }}"
|
||||
ng-click="groupState !== 'expanded' && toggleShowDuplicateJobs()"
|
||||
ng-disabled="groupState === 'expanded'"
|
||||
ng-class="{ 'strikethrough': !showDuplicateJobs }">
|
||||
</span>
|
||||
|
||||
<span class="btn-group">
|
||||
<!--Toggle Group State Button-->
|
||||
<span class="btn btn-view-nav btn-sm btn-toggle-group-state"
|
||||
tabindex="0" role="button"
|
||||
title="{{ groupState === 'collapsed' ? 'Expand job groups' : 'Collapse job groups' }}"
|
||||
ng-click="toggleGroupState()">(
|
||||
<span class="group-state-nav-icon">
|
||||
{{ groupState === 'collapsed' ? "+" : "-" }}</span>
|
||||
)</span>
|
||||
</span>
|
||||
|
||||
<!--Result Status Filter Chicklets-->
|
||||
<span class="resultStatusChicklets">
|
||||
<ng-include src="'partials/main/thFilterChicklets.html'"></ng-include>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span class="btn btn-view-nav btn-sm"
|
||||
ng-click="toggleFieldFilterVisible()"
|
||||
title="Filter by a job field">
|
||||
<i class="fa fa-filter" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!--Quick Filter Field-->
|
||||
<span ng-controller="SearchCtrl"
|
||||
id="quick-filter-parent"
|
||||
class="form-group form-inline">
|
||||
<input id="quick-filter"
|
||||
class="form-control form-control-sm" required
|
||||
title="Click to enter filter values"
|
||||
ng-model="searchQueryStr" ng-keydown="search($event)" type="text"
|
||||
placeholder="Filter platforms & jobs"
|
||||
blur-this>
|
||||
<span id="quick-filter-clear-button"
|
||||
class="fa fa-times-circle"
|
||||
title="Clear this filter"
|
||||
ng-click="clearFilterBox()"></span>
|
||||
</span>
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactLinkify, { linkify } from 'react-linkify';
|
||||
|
||||
export default class BugLinkify extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
linkify.add('bug:', {
|
||||
validate: function (text, pos, self) {
|
||||
const bugNumber = text.slice(pos).split(' ')[0];
|
||||
|
||||
self.re.bug = /^([0-9]+)/ig;
|
||||
if (self.re.bug.test(bugNumber)) {
|
||||
return bugNumber.match(self.re.bug)[0].length;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
normalize: (match) => {
|
||||
const bugNumber = match.text.replace('bug:', '');
|
||||
|
||||
match.url = 'https://bugzilla.mozilla.org/show_bug.cgi?id=' + bugNumber;
|
||||
match.text = `Bug ${bugNumber}`;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getBugsAsLinkProtocol(text) {
|
||||
let bugText = text;
|
||||
const bugMatches = text.match(/-- ([0-9]+)|bug.([0-9]+)/ig);
|
||||
const bugProtocol = 'bug:$1';
|
||||
|
||||
if (bugMatches) {
|
||||
// Need a pass for each matching style for if there are multiple styles
|
||||
// in the string.
|
||||
bugText = bugText.replace(/Bug ([0-9]+)/g, bugProtocol);
|
||||
bugText = bugText.replace(/bug ([0-9]+)/g, bugProtocol);
|
||||
bugText = bugText.replace(/-- ([0-9]+)/g, bugProtocol);
|
||||
}
|
||||
return bugText;
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ReactLinkify properties={{ target: '_blank', rel: 'noopener noreferrer' }}>
|
||||
{this.getBugsAsLinkProtocol(this.props.children)}
|
||||
</ReactLinkify>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BugLinkify.propTypes = {
|
||||
children: PropTypes.string.isRequired,
|
||||
};
|
22
yarn.lock
22
yarn.lock
|
@ -4519,6 +4519,12 @@ levn@^0.3.0, levn@~0.3.0:
|
|||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
|
||||
linkify-it@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.3.tgz#d94a4648f9b1c179d64fa97291268bdb6ce9434f"
|
||||
dependencies:
|
||||
uc.micro "^1.0.1"
|
||||
|
||||
load-json-file@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
|
||||
|
@ -6201,6 +6207,14 @@ react-lifecycles-compat@^3.0.4:
|
|||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
|
||||
react-linkify@0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/react-linkify/-/react-linkify-0.2.2.tgz#55b99b1cc7244446a0f9bdebbe13b2c30f789e65"
|
||||
dependencies:
|
||||
linkify-it "^2.0.3"
|
||||
prop-types "^15.5.8"
|
||||
tlds "^1.57.0"
|
||||
|
||||
react-popper@^0.10.4:
|
||||
version "0.10.4"
|
||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.10.4.tgz#af2a415ea22291edd504678d7afda8a6ee3295aa"
|
||||
|
@ -7262,6 +7276,10 @@ timers-browserify@^2.0.4:
|
|||
dependencies:
|
||||
setimmediate "^1.0.4"
|
||||
|
||||
tlds@^1.57.0:
|
||||
version "1.203.1"
|
||||
resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.203.1.tgz#4dc9b02f53de3315bc98b80665e13de3edfc1dfc"
|
||||
|
||||
tmp@0.0.31:
|
||||
version "0.0.31"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
|
||||
|
@ -7345,6 +7363,10 @@ ua-parser-js@^0.7.18:
|
|||
version "0.7.18"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
|
||||
|
||||
uc.micro@^1.0.1:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376"
|
||||
|
||||
uglify-js@3.4.x:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.3.tgz#a4fd757b6f34a95f717f7a7a0dccafcaaa60cf7f"
|
||||
|
|
Загрузка…
Ссылка в новой задаче