-
Notifications
You must be signed in to change notification settings - Fork 45
Add search bar and filters for logcat output #147
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
<div class="content"> | ||
<div id="static_logs" class="content"> | ||
<div class="card log"> | ||
${log_entries} | ||
</div> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import React, { Component } from 'react'; | ||
import cx from 'classnames'; | ||
import LogFilter from './LogFilter' | ||
|
||
export default class LogContainer extends Component { | ||
|
||
state = { | ||
logs: [], | ||
results: [], | ||
loading: true, | ||
hide: false | ||
} | ||
|
||
componentDidMount() { | ||
document.addEventListener('DOMContentLoaded', (event) => { | ||
this.loadStaticLogs() | ||
}) | ||
} | ||
|
||
loadStaticLogs() { | ||
let logDivs = document.querySelectorAll('#static_logs > div > div') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a bad practice to reach elements directly from DOM in react, because it is against the whole concept of virtual DOM, except for rare cases (like reaching out |
||
if (!logDivs.length) { | ||
this.setState({hide: true, loading: false}) | ||
return | ||
} | ||
let logs = [] | ||
for (var div of logDivs) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't use Also, you can use method |
||
logs.push(Object.assign({level: div.className, line: div.innerText, key: logs.length})) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no sense in using Also, if you want to add a unique key, here comes the |
||
} | ||
this.setState({logs: logs, results: logs, loading: false}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fyi, in es6, if a key is the same as a value, you can write it like this: |
||
window.document.getElementById("static_logs").remove() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changing DOM directly with JS in React is a forbidden action and it is against basic React rules. Please read the React manual for more information. |
||
} | ||
|
||
getSearchResults(results) { | ||
this.setState({ results: results }); | ||
} | ||
|
||
render() { | ||
return this.state.hide | ||
? (<div></div>) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In React, if we don't need a component to render, we return |
||
: ( | ||
<div className="margin-top-20"> | ||
<LogFilter | ||
setSearchResults={ (results) => this.getSearchResults(results) } | ||
data={this.state.logs} | ||
/> | ||
{ !this.state.loading && | ||
<div className="card log"> | ||
{ this.state.results.map((entry, i) => { | ||
return ( | ||
<div key={ entry.key } className={ entry.level }> | ||
{ entry.line } | ||
</div> | ||
) | ||
}) | ||
} | ||
</div>} | ||
</div> | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import React, { Component } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import cx from 'classnames'; | ||
import SearchBar from './SearchBar'; | ||
import LogFilterButton from './LogFilterButton'; | ||
|
||
const SEARCH_FIELDS = ['level', 'line']; | ||
const SEARCH_REF = 'line'; | ||
export default class LogFilter extends Component { | ||
static propTypes = { | ||
setSearchResults: PropTypes.func, | ||
data: PropTypes.array | ||
}; | ||
|
||
render() { | ||
return ( | ||
<div> | ||
<div className="card"> | ||
<div className="title-common">LOGS</div> | ||
<LogFilterButton onClick={ () => this.performFilterSearch("level:verbose") } text="Verbose" disabled={ !!!this.props.data.length } /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using |
||
<LogFilterButton onClick={ () => this.performFilterSearch("level:debug") } text="Debug" disabled={ !!!this.props.data.length } /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you use smth more than 1 time — put it into a variable. |
||
<LogFilterButton onClick={ () => this.performFilterSearch("level:info") } text="Info" disabled={ !!!this.props.data.length } /> | ||
<LogFilterButton onClick={ () => this.performFilterSearch("level:warning") } text="Warning" disabled={ !!!this.props.data.length } /> | ||
<LogFilterButton onClick={ () => this.performFilterSearch("level:error") } text="Error" disabled={ !!!this.props.data.length } /> | ||
<LogFilterButton onClick={ () => this.performFilterSearch("level:assert") } text="Assert" disabled={ !!!this.props.data.length } /> | ||
</div> | ||
<SearchBar | ||
setSearchResults={this.props.setSearchResults} | ||
searchFields={SEARCH_FIELDS} | ||
searchRef={SEARCH_REF} | ||
data={this.props.data} | ||
setPerformFilterSearchCallback={ callback => (this.performFilterSearch = callback) } | ||
/> | ||
</div> | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import React from 'react'; | ||
|
||
const LogFilterButton = ({text, disabled, onClick}) => ( | ||
<button | ||
className="button secondary margin-right-10" | ||
onClick={ onClick } | ||
disabled={ disabled } | ||
> | ||
{text} | ||
</button> | ||
); | ||
|
||
export default LogFilterButton; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,30 +4,47 @@ import cx from 'classnames'; | |
import elasticlunr from 'elasticlunr'; | ||
import convertTime from './../utils/convertTime' | ||
|
||
const SEARCH_FIELDS = ['package_name', 'class_name', 'name', 'id', 'status']; | ||
const SEARCH_REF = 'id'; | ||
const EL_SEARCH = elasticlunr(); | ||
export default class SearchBar extends Component { | ||
static propTypes = { | ||
setSearchResults: PropTypes.func | ||
setSearchResults: PropTypes.func, | ||
setPerformFilterSearchCallback: PropTypes.func, | ||
searchFields: PropTypes.array, | ||
searchRef: PropTypes.string, | ||
data: PropTypes.array, | ||
}; | ||
|
||
state = { | ||
data: window.suite.tests, | ||
data: this.props.data, | ||
error: false, | ||
searchLabel: null, | ||
searchParams: null, | ||
query: '' | ||
}; | ||
|
||
componentWillMount() { | ||
let { data } = this.state; | ||
elasticlunr.clearStopWords(); | ||
SEARCH_FIELDS.forEach(f => EL_SEARCH.addField(f)) | ||
EL_SEARCH.setRef(SEARCH_REF); | ||
if (data.length) { | ||
data.forEach(item => EL_SEARCH.addDoc(item)) | ||
} | ||
this.props.searchFields.forEach(f => EL_SEARCH.addField(f)) | ||
EL_SEARCH.setRef(this.props.searchRef); | ||
} | ||
|
||
componentDidMount() { | ||
this.props.setPerformFilterSearchCallback(this.performFilterSearch) | ||
} | ||
|
||
componentWillReceiveProps(props) { | ||
let data = props.data; | ||
if (data === this.state.data) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code will always result in |
||
return; | ||
} | ||
if (data.length) { | ||
data.forEach(item => EL_SEARCH.addDoc(item)) | ||
} | ||
this.setState({data: props.data}); | ||
} | ||
|
||
componentWillUnmount() { | ||
this.props.setPerformFilterSearchCallback(undefined) | ||
} | ||
|
||
mapResults(results) { | ||
|
@@ -42,14 +59,14 @@ export default class SearchBar extends Component { | |
}; | ||
|
||
setTagSearch = (field, callback) => { | ||
if (SEARCH_FIELDS.indexOf(field) < 0) { | ||
if (this.props.searchFields.indexOf(field) < 0) { | ||
this.setState({ error: true }); | ||
return; | ||
} | ||
|
||
let params = {}; | ||
params.fields = {}; | ||
SEARCH_FIELDS.forEach((f) => { | ||
this.props.searchFields.forEach((f) => { | ||
if (f === field) { | ||
params.fields[f] = { boost: 1 } | ||
} else { | ||
|
@@ -97,43 +114,29 @@ export default class SearchBar extends Component { | |
render() { | ||
let errorTextClasses = cx('form-item__error-text col-100', { visible: this.state.error }); | ||
let errorInputClasses = cx({ 'is-invalid-input': this.state.error }); | ||
const data = window.suite; | ||
|
||
return ( | ||
<div> | ||
<div className="row justify-between"> | ||
<div className="card card-info filter-card" onClick={ () => this.performFilterSearch('status:passed') }> | ||
<div className="text-sub-title-light">Passed</div> | ||
<div className="card-info__content status-passed">{ data.passed_count }</div> | ||
</div> | ||
<div className="card card-info filter-card" onClick={ () => this.performFilterSearch('status:failed') }> | ||
<div className="text-sub-title-light">Failed</div> | ||
<div className="card-info__content status-failed">{ data.failed_count }</div> | ||
</div> | ||
<div className="card card-info filter-card" onClick={ () => this.performFilterSearch('status:ignored') }> | ||
<div className="text-sub-title-light">Ignored</div> | ||
<div className="card-info__content status-ignored">{ data.ignored_count }</div> | ||
</div> | ||
<div className="card card-info"> | ||
<div className="text-sub-title-light">Duration</div> | ||
<div className="card-info__content">{ convertTime(data.duration_millis) }</div> | ||
</div> | ||
</div> | ||
<div className="card"> | ||
<div className="form-container"> | ||
<div className="row search-params full"> | ||
<div className="row full-width-content input-group full"> | ||
<div className="form-item"> | ||
<div className="vertical-aligned-content"> | ||
{ this.state.searchLabel && <div className="label margin-right-20">{ this.state.searchLabel }:</div> } | ||
<input type="text" className={ errorInputClasses } placeholder="Search" value={ this.state.query } | ||
onChange={ this.setSearchQuery } /> | ||
<button type="reset" className="button secondary margin-left-20" onClick={ this.clearResults }> | ||
Reset | ||
</button> | ||
</div> | ||
<div className={ errorTextClasses }>No such key exists!</div> | ||
<div className="card"> | ||
<div className="form-container"> | ||
<div className="row search-params full"> | ||
<div className="row full-width-content input-group full"> | ||
<div className="form-item"> | ||
<div className="vertical-aligned-content"> | ||
{ this.state.searchLabel && <div className="label margin-right-20">{ this.state.searchLabel }:</div> } | ||
<input | ||
type="text" className={ errorInputClasses } placeholder="Search" value={ this.state.query } | ||
onChange={ this.setSearchQuery } | ||
disabled={ !!!this.state.data.length ? "disabled" : "" } | ||
/> | ||
<button | ||
type="reset" className="button secondary margin-left-20" | ||
onClick={ this.clearResults } | ||
disabled={ !!!this.state.data.length ? "disabled" : "" } | ||
> | ||
Reset | ||
</button> | ||
</div> | ||
<div className={ errorTextClasses }>No such key exists!</div> | ||
</div> | ||
</div> | ||
</div> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import React, { Component } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import cx from 'classnames'; | ||
import convertTime from './../utils/convertTime' | ||
import SearchBar from './SearchBar'; | ||
|
||
const SEARCH_FIELDS = ['package_name', 'class_name', 'name', 'id', 'status']; | ||
const SEARCH_REF = 'id'; | ||
export default class SuiteFilter extends Component { | ||
static propTypes = { | ||
setSearchResults: PropTypes.func | ||
}; | ||
|
||
performFilterSearch = (query) => { | ||
this.searchBar.performFilterSearch(query) | ||
}; | ||
|
||
render() { | ||
const data = window.suite; | ||
|
||
return ( | ||
<div> | ||
<div className="row justify-between"> | ||
<div className="card card-info filter-card" onClick={ () => this.performFilterSearch('status:passed') }> | ||
<div className="text-sub-title-light">Passed</div> | ||
<div className="card-info__content status-passed">{ data.passed_count }</div> | ||
</div> | ||
<div className="card card-info filter-card" onClick={ () => this.performFilterSearch('status:failed') }> | ||
<div className="text-sub-title-light">Failed</div> | ||
<div className="card-info__content status-failed">{ data.failed_count }</div> | ||
</div> | ||
<div className="card card-info filter-card" onClick={ () => this.performFilterSearch('status:ignored') }> | ||
<div className="text-sub-title-light">Ignored</div> | ||
<div className="card-info__content status-ignored">{ data.ignored_count }</div> | ||
</div> | ||
<div className="card card-info"> | ||
<div className="text-sub-title-light">Duration</div> | ||
<div className="card-info__content">{ convertTime(data.duration_millis) }</div> | ||
</div> | ||
</div> | ||
<SearchBar | ||
setSearchResults={this.props.setSearchResults} | ||
searchFields={SEARCH_FIELDS} | ||
searchRef={SEARCH_REF} | ||
data={data.tests} | ||
setPerformFilterSearchCallback={ callback => (this.performFilterSearch = callback) } | ||
/> | ||
</div> | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It has no sense adding listener
DOMContentLoaded
as the methodcomponentDidMount
itself says that the DOM is ready. Please read the React documentation to understand React way: https://reactjs.org/docs/state-and-lifecycle.html