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:
Cameron Dawson 2018-07-13 16:04:51 -07:00 коммит произвёл GitHub
Родитель 4ccadb29f4
Коммит bc4e8a7b14
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
29 изменённых файлов: 639 добавлений и 597 удалений

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

@ -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" />&nbsp;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>&nbsp;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>

55
ui/shared/BugLinkify.jsx Normal file
Просмотреть файл

@ -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,
};

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

@ -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"