Skip to content

Commit

Permalink
Bug 1469917 - Convert the Active Filters bar to React (#3690)
Browse files Browse the repository at this point in the history
  • Loading branch information
Cameron Dawson committed Jun 28, 2018
1 parent b00d42c commit b7d5fa7
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 170 deletions.
1 change: 0 additions & 1 deletion ui/index.html
Expand Up @@ -24,7 +24,6 @@
</ng-include>
</div>
<ng-include src="'partials/main/thTreeherderUpdateBar.html'"></ng-include>
<ng-include src="'partials/main/thActiveFiltersBar.html'"></ng-include>
<job-view
revision="revision"
current-repo="currentRepo"
Expand Down
108 changes: 81 additions & 27 deletions ui/job-view/JobView.jsx
@@ -1,38 +1,92 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular/index.es2015';
import { createBrowserHistory } from 'history';

import treeherder from '../js/treeherder';
import { thEvents } from '../js/constants';
import DetailsPanel from './details/DetailsPanel';
import ActiveFilters from './navbars/ActiveFilters';
import PushList from './PushList';

const JobView = (props) => {
const { user, repoName, revision, currentRepo, selectedJob, $injector } = props;

return (
<React.Fragment>
<div id="th-global-content" className="th-global-content">
<span className="th-view-content">
<PushList
user={user}
repoName={repoName}
revision={revision}
currentRepo={currentRepo}
$injector={$injector}
/>
</span>
</div>
<DetailsPanel
className={selectedJob ? '' : 'hidden'}
currentRepo={currentRepo}
repoName={repoName}
selectedJob={selectedJob}
user={user}
$injector={$injector}
/>
</React.Fragment>
);
};
class JobView extends React.Component {
constructor(props) {
super(props);

const { $injector } = this.props;
this.thJobFilters = $injector.get('thJobFilters');
this.$rootScope = $injector.get('$rootScope');

this.history = createBrowserHistory();

this.state = {
isFieldFilterVisible: false,
filterBarFilters: [
...this.thJobFilters.getNonFieldFiltersArray(),
...this.thJobFilters.getFieldFiltersArray(),
],
};
}

componentDidMount() {
this.toggleFieldFilterVisible = this.toggleFieldFilterVisible.bind(this);

this.$rootScope.$on(thEvents.toggleFieldFilterVisible, () => {
this.toggleFieldFilterVisible();
});

this.history.listen(() => {
this.setState({
filterBarFilters: [
...this.thJobFilters.getNonFieldFiltersArray(),
...this.thJobFilters.getFieldFiltersArray(),
],
});
});
}

toggleFieldFilterVisible() {
this.setState({ isFieldFilterVisible: !this.state.isFieldFilterVisible });
}

render() {
const {
user, repoName, revision, currentRepo, selectedJob, $injector,
} = this.props;
const { isFieldFilterVisible, filterBarFilters } = this.state;

return (
<React.Fragment>
{(isFieldFilterVisible || !!filterBarFilters.length) && <ActiveFilters
$injector={$injector}
filterBarFilters={filterBarFilters}
history={this.history}
isFieldFilterVisible={isFieldFilterVisible}
toggleFieldFilterVisible={this.toggleFieldFilterVisible}
/>}
<div id="th-global-content" className="th-global-content">
<span className="th-view-content">
<PushList
user={user}
repoName={repoName}
revision={revision}
currentRepo={currentRepo}
$injector={$injector}
/>
</span>
</div>
<DetailsPanel
className={selectedJob ? '' : 'hidden'}
currentRepo={currentRepo}
repoName={repoName}
selectedJob={selectedJob}
user={user}
$injector={$injector}
/>
</React.Fragment>
);
}
}

JobView.propTypes = {
$injector: PropTypes.object.isRequired,
Expand Down
164 changes: 164 additions & 0 deletions ui/job-view/navbars/ActiveFilters.jsx
@@ -0,0 +1,164 @@
import React from 'react';
import PropTypes from 'prop-types';

export default class ActiveFilters extends React.Component {
constructor(props) {
super(props);

const { $injector } = this.props;
this.thJobFilters = $injector.get('thJobFilters');
this.fieldChoices = this.thJobFilters.getFieldChoices();

this.state = {
newFilterField: '',
newFilterMatchType: '',
newFilterValue: '',
newFilterChoices: [],
};
}

componentDidMount() {
this.addNewFieldFilter = this.addNewFieldFilter.bind(this);
this.setNewFilterValue = this.setNewFilterValue.bind(this);
this.setNewFilterField = this.setNewFilterField.bind(this);
this.clearNewFieldFilter = this.clearNewFieldFilter.bind(this);
}

setNewFilterField(field) {
this.setState({
newFilterField: field,
newFilterMatchType: this.fieldChoices[field].matchType,
newFilterChoices: this.fieldChoices[field].choices,
});
}

setNewFilterValue(value) {
this.setState({ newFilterValue: value });
}

getFilterValue(field, value) {
const choice = this.fieldChoices[field];
const choiceValue = choice.choices[value];

return choice.matchType === 'choice' && choiceValue ? choiceValue.name : value;
}

addNewFieldFilter() {
const { newFilterField, newFilterValue } = this.state;

if (newFilterField && newFilterValue) {
this.thJobFilters.addFilter(newFilterField, newFilterValue);
this.clearNewFieldFilter();
}
}

// Clear the values and close the input form group
clearNewFieldFilter() {
this.setState({
newFilterField: '',
newFilterMatchType: '',
newFilterValue: '',
newFilterChoices: [],
});
this.props.toggleFieldFilterVisible();
}

render() {
const { filterBarFilters, isFieldFilterVisible } = this.props;
const {
newFilterField, newFilterMatchType, newFilterValue, newFilterChoices,
} = this.state;

return (
<div className="alert-info active-filters-bar">
{!!filterBarFilters.length && <div>
<span
className="pointable"
title="Clear all of these filters"
onClick={this.thJobFilters.clearAllFilters}
><i className="fa fa-times-circle" /> </span>
<span className="active-filters-title">
<b>Active Filters</b>
</span>
{filterBarFilters.map(filter => (
<span className="filtersbar-filter" key={filter.key}>
<span
className="pointable"
title={`Clear filter: ${filter.field}`}
onClick={() => this.thJobFilters.removeFilter(filter.key, filter.value)}
>
<i className="fa fa-times-circle" />&nbsp;
</span>
<span title={`Filter by ${filter.field}: ${filter.value}`}>
<b>{filter.field}:</b>
{filter.field === 'failure_classification_id' && (
<span> {this.getFilterValue(filter.field, filter.value)}</span>
)}
{filter.field === 'author' && <span> {filter.value.split('@')[0].substr(0, 20)}</span>}
{filter.field !== 'author' && filter.field !== 'failure_classification_id' && <span> {filter.value.substr(0, 12)}</span>}
</span>
</span>))}
</div>}
{isFieldFilterVisible && <div>
<form className="form-inline">
<div className="form-group input-group-sm new-filter-input">
<label className="sr-only" htmlFor="job-filter-field">Field</label>
<select
id="job-filter-field"
className="form-control"
value={newFilterField}
onChange={evt => this.setNewFilterField(evt.target.value)}
placeholder="filter field"
required
>
<option value="" disabled>select filter field</option>
{Object.entries(this.fieldChoices).map(([field, obj]) => (
obj.name !== 'tier' ? <option value={field} key={field}>{obj.name}</option> : null
))}
</select>
<label className="sr-only" htmlFor="job-filter-value">Value</label>
{newFilterMatchType !== 'choice' && <input
className="form-control"
value={newFilterValue}
onChange={evt => this.setNewFilterValue(evt.target.value)}
id="job-filter-value"
type="text"
required
placeholder="enter filter value"
/>}
<label className="sr-only" htmlFor="job-filter-choice-value">Value</label>
{newFilterMatchType === 'choice' && <select
className="form-control"
value={newFilterValue}
onChange={evt => this.setNewFilterValue(evt.target.value)}
id="job-filter-choice-value"
>
<option value="" disabled>select value</option>
{Object.entries(newFilterChoices).map(([fci, fci_obj]) => (
<option value={fci} key={fci}>{fci_obj.name}</option>
)) }
</select>}
<button
type="submit"
className="btn btn-light-bordered btn-sm"
onClick={this.addNewFieldFilter}
>add</button>
<button
type="reset"
className="btn btn-light-bordered btn-sm"
onClick={this.clearNewFieldFilter}
>cancel</button>
</div>
</form>
</div>}
</div>
);
}
}

ActiveFilters.propTypes = {
$injector: PropTypes.object.isRequired,
filterBarFilters: PropTypes.array.isRequired,
isFieldFilterVisible: PropTypes.bool.isRequired,
toggleFieldFilterVisible: PropTypes.func.isRequired,
};
1 change: 1 addition & 0 deletions ui/js/constants.js
Expand Up @@ -311,6 +311,7 @@ export const thEvents = {
autoclassifyToggleEdit: 'ac-toggle-edit-EVT',
autoclassifyOpenLogViewer: 'ac-open-log-viewer-EVT',
selectRunnableJob: 'select-runnable-job-EVT',
toggleFieldFilterVisible: 'toggle-field-filter-visible-EVT',
};

export const phCompareDefaultOriginalRepo = 'mozilla-central';
Expand Down
62 changes: 2 additions & 60 deletions ui/js/controllers/main.js
Expand Up @@ -514,64 +514,8 @@ treeherderApp.controller('MainCtrl', [
);
};

$scope.getFiltersForBar = function () {
return [...thJobFilters.getNonFieldFiltersArray(), ...thJobFilters.getFieldFiltersArray()];
};

// field filters
$scope.newFieldFilter = null;
$scope.fieldFilters = [];
$scope.fieldChoices = thJobFilters.getFieldChoices();

$scope.toggleFieldFilterVisibility = function () {
if ($scope.newFieldFilter === null) {
$scope.newFieldFilter = { field: '', value: '' };
}
$scope.isFieldFilterVisible = !$scope.isFieldFilterVisible;
};

$scope.cancelNewFieldFilter = function () {
$scope.newFieldFilter = null;
$scope.isFieldFilterVisible = !$scope.isFieldFilterVisible;
};

// we have to set the field match type here so that the UI can either
// show a text field for entering a value, or switch to a drop-down select.
$scope.setFieldMatchType = function () {
if ($scope.newFieldFilter.field) {
$scope.newFieldFilter.matchType = $scope.fieldChoices[$scope.newFieldFilter.field].matchType;
$scope.newFieldFilter.choices = $scope.fieldChoices[$scope.newFieldFilter.field].choices;
}
};

// for most match types we want to show just the raw value. But for
// choice value type, we want to show the string representation of the
// value. For example, failure_classification_id is an int, but we
// want to show the text.
$scope.getFilterValue = function (field, value) {
if ($scope.fieldChoices[field].matchType === 'choice' &&
$scope.fieldChoices[field].choices[value]) {
return $scope.fieldChoices[field].choices[value].name;
}
return value;
};

$scope.addNewFieldFilter = function () {
if (!$scope.newFieldFilter) {
return;
}

const { value, field } = $scope.newFieldFilter;

if (field === '' || value === '') {
return;
}

thJobFilters.addFilter(field, value);

// Clear the values and close the input form group
$scope.newFieldFilter = { field: '', value: '' };
$scope.isFieldFilterVisible = !$scope.isFieldFilterVisible;
$scope.toggleFieldFilterVisible = function () {
$rootScope.$emit(thEvents.toggleFieldFilterVisible);
};

$scope.fromChangeValue = function () {
Expand Down Expand Up @@ -604,8 +548,6 @@ treeherderApp.controller('MainCtrl', [
// used to avoid bad urls when the app redirects internally
$rootScope.urlBasePath = $location.absUrl().split('?')[0];

$scope.filterBarFilters = $scope.getFiltersForBar();

const newReloadTriggerParams = getNewReloadTriggerParams();
// if we are just setting the repo to the default because none was
// set initially, then don't reload the page.
Expand Down

0 comments on commit b7d5fa7

Please sign in to comment.