Refactor MeasurementList

pull/2914/head
Andrew Watkins 2018-03-03 23:05:40 -08:00
parent 5f26de0d96
commit 52fcc8e95f
4 changed files with 182 additions and 131 deletions

View File

@ -1,6 +1,5 @@
import React, {PureComponent} from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import _ from 'lodash'
import {showMeasurements} from 'src/shared/apis/metaQuery'
@ -8,7 +7,8 @@ import showMeasurementsParser from 'src/shared/parsing/showMeasurements'
import {Query, Source} from 'src/types'
import TagList from 'src/data_explorer/components/TagList'
import MeasurementListFilter from 'src/shared/components/MeasurementListFilter'
import MeasurementListItem from 'src/shared/components/MeasurementListItem'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
interface Props {
@ -23,6 +23,7 @@ interface Props {
interface State {
measurements: string[]
filterText: string
filtered: string[]
}
const {shape, string} = PropTypes
@ -31,6 +32,7 @@ class MeasurementList extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
filtered: [],
measurements: [],
filterText: '',
}
@ -38,6 +40,8 @@ class MeasurementList extends PureComponent<Props, State> {
this.handleEscape = this.handleEscape.bind(this)
this.handleFilterText = this.handleFilterText.bind(this)
this.handleAcceptReject = this.handleAcceptReject.bind(this)
this.handleFilterMeasuremet = this.handleFilterMeasuremet.bind(this)
this.handleChoosemeasurement = this.handleChoosemeasurement.bind(this)
}
public static defaultProps: Partial<Props> = {
@ -57,7 +61,7 @@ class MeasurementList extends PureComponent<Props, State> {
return
}
this._getMeasurements()
this.getMeasurements()
}
componentDidUpdate(prevProps) {
@ -74,16 +78,24 @@ class MeasurementList extends PureComponent<Props, State> {
return
}
this._getMeasurements()
this.getMeasurements()
}
handleFilterText(e) {
e.stopPropagation()
const filterText = e.target.value
this.setState({
filterText: e.target.value,
filterText,
filtered: this.handleFilterMeasuremet(filterText),
})
}
handleFilterMeasuremet(filter) {
return this.state.measurements.filter(m =>
m.toLowerCase().includes(filter.toLowerCase())
)
}
handleEscape(e) {
if (e.key !== 'Escape') {
return
@ -99,122 +111,69 @@ class MeasurementList extends PureComponent<Props, State> {
this.props.onToggleTagAcceptance()
}
handleChoosemeasurement(measurement) {
return () => this.props.onChooseMeasurement(measurement)
}
render() {
const {query, querySource, onChooseTag, onGroupByTag} = this.props
const {database, areTagsAccepted} = query
const {filtered} = this.state
return (
<div className="query-builder--column">
<div className="query-builder--heading">
<span>Measurements & Tags</span>
{this.props.query.database
? <div className="query-builder--filter">
<input
className="form-control input-sm"
placeholder="Filter"
type="text"
value={this.state.filterText}
onChange={this.handleFilterText}
onKeyUp={this.handleEscape}
spellCheck={false}
autoComplete="false"
/>
<span className="icon search" />
</div>
: null}
{database &&
<MeasurementListFilter
onEscape={this.handleEscape}
onFilterText={this.handleFilterText}
filterText={this.state.filterText}
/>}
</div>
{this.renderList()}
{database
? <div className="query-builder--list">
<FancyScrollbar>
{filtered.map(measurement =>
<MeasurementListItem
query={query}
key={measurement}
measurement={measurement}
querySource={querySource}
onChooseTag={onChooseTag}
onGroupByTag={onGroupByTag}
areTagsAccepted={areTagsAccepted}
onAcceptReject={this.handleAcceptReject}
isActive={measurement === query.measurement}
numTagsActive={Object.keys(query.tags).length}
onChooseMeasurement={this.handleChoosemeasurement}
/>
)}
</FancyScrollbar>
</div>
: <div className="query-builder--list-empty">
<span>
No <strong>Database</strong> selected
</span>
</div>}
</div>
)
}
renderList() {
if (!this.props.query.database) {
return (
<div className="query-builder--list-empty">
<span>
No <strong>Database</strong> selected
</span>
</div>
)
}
const filterText = this.state.filterText.toLowerCase()
const measurements = this.state.measurements.filter(m =>
m.toLowerCase().includes(filterText)
)
return (
<div className="query-builder--list">
<FancyScrollbar>
{measurements.map(measurement => {
const isActive = measurement === this.props.query.measurement
const numTagsActive = Object.keys(this.props.query.tags).length
return (
<div
key={measurement}
onClick={
isActive
? () => {}
: () => this.props.onChooseMeasurement(measurement)
}
>
<div
className={classnames('query-builder--list-item', {
active: isActive,
})}
data-test={`query-builder-list-item-measurement-${measurement}`}
>
<span>
<div className="query-builder--caret icon caret-right" />
{measurement}
</span>
{isActive && numTagsActive >= 1
? <div
className={classnames('flip-toggle', {
flipped: this.props.query.areTagsAccepted,
})}
onClick={this.handleAcceptReject}
>
<div className="flip-toggle--container">
<div className="flip-toggle--front">!=</div>
<div className="flip-toggle--back">=</div>
</div>
</div>
: null}
</div>
{isActive
? <TagList
query={this.props.query}
querySource={this.props.querySource}
onChooseTag={this.props.onChooseTag}
onGroupByTag={this.props.onGroupByTag}
/>
: null}
</div>
)
})}
</FancyScrollbar>
</div>
)
}
_getMeasurements() {
async getMeasurements() {
const {source} = this.context
const {querySource} = this.props
const {querySource, query} = this.props
const proxy =
_.get(querySource, ['links', 'proxy'], null) || source.links.proxy
const proxy = _.get(querySource, ['links', 'proxy'], source.links.proxy)
showMeasurements(proxy, this.props.query.database).then(resp => {
const {errors, measurementSets} = showMeasurementsParser(resp.data)
if (errors.length) {
// TODO: display errors in the UI.
return console.error('InfluxDB returned error(s): ', errors)
}
this.setState({
measurements: measurementSets[0].measurements,
})
})
try {
const {data} = await showMeasurements(proxy, query.database)
const {measurementSets} = showMeasurementsParser(data)
const measurements = measurementSets[0].measurements
this.setState({measurements, filtered: measurements})
} catch (err) {
console.error(err)
}
}
}

View File

@ -0,0 +1,28 @@
import React, {SFC} from 'react'
interface Props {
filterText: string
onEscape: (e: React.KeyboardEvent<HTMLInputElement>) => void
onFilterText: (e: React.InputHTMLAttributes<HTMLInputElement>) => void
}
const MeasurementListFilter: SFC<Props> = ({
onEscape,
onFilterText,
filterText,
}) =>
<div className="query-builder--filter">
<input
className="form-control input-sm"
placeholder="Filter"
type="text"
value={filterText}
onChange={onFilterText}
onKeyUp={onEscape}
spellCheck={false}
autoComplete="false"
/>
<span className="icon search" />
</div>
export default MeasurementListFilter

View File

@ -0,0 +1,64 @@
import React, {SFC} from 'react'
import classnames from 'classnames'
import TagList from 'src/data_explorer/components/TagList'
import {Query, Source} from 'src/types'
interface Props {
query: Query
querySource: Source
isActive: boolean
measurement: string
numTagsActive: number
areTagsAccepted: boolean
onChooseTag: () => void
onGroupByTag: () => void
onAcceptReject: () => void
onChooseMeasurement: (measurement: string) => () => void
}
const noop = () => {}
const MeasurementListItem: SFC<Props> = ({
query,
isActive,
querySource,
measurement,
onChooseTag,
onGroupByTag,
numTagsActive,
onAcceptReject,
areTagsAccepted,
onChooseMeasurement,
}) =>
<div
key={measurement}
onClick={isActive ? noop : onChooseMeasurement(measurement)}
>
<div className={classnames('query-builder--list-item', {active: isActive})}>
<span>
<div className="query-builder--caret icon caret-right" />
{measurement}
</span>
{isActive &&
numTagsActive >= 1 &&
<div
className={classnames('flip-toggle', {flipped: areTagsAccepted})}
onClick={onAcceptReject}
>
<div className="flip-toggle--container">
<div className="flip-toggle--front">!=</div>
<div className="flip-toggle--back">=</div>
</div>
</div>}
</div>
{isActive &&
<TagList
query={query}
querySource={querySource}
onChooseTag={onChooseTag}
onGroupByTag={onGroupByTag}
/>}
</div>
export default MeasurementListItem

View File

@ -14,7 +14,7 @@ const setup = (override = {}) => {
...override,
}
MeasurementList.prototype._getMeasurements = jest.fn(() => Promise.resolve())
MeasurementList.prototype.getMeasurements = jest.fn(() => Promise.resolve())
const wrapper = shallow(<MeasurementList {...props} />, {
context: {source},
@ -41,67 +41,67 @@ describe('Shared.Components.MeasurementList', () => {
describe('lifecycle methods', () => {
describe('componentDidMount', () => {
it('does not fire getMeasurements if there is no database', () => {
const _getMeasurements = jest.fn()
const getMeasurements = jest.fn()
const {instance} = setup({query: {...query, database: ''}})
instance._getMeasurements = _getMeasurements
instance.getMeasurements = getMeasurements
instance.componentDidMount()
expect(_getMeasurements).not.toHaveBeenCalled()
expect(getMeasurements).not.toHaveBeenCalled()
})
it('does fire _getMeasurements if there is a database', () => {
const _getMeasurements = jest.fn()
it('does fire getMeasurements if there is a database', () => {
const getMeasurements = jest.fn()
const {instance} = setup()
instance._getMeasurements = _getMeasurements
instance.getMeasurements = getMeasurements
instance.componentDidMount()
expect(_getMeasurements).toHaveBeenCalled()
expect(getMeasurements).toHaveBeenCalled()
})
})
describe('componentDidUpdate', () => {
it('does not fire getMeasurements if there is no database', () => {
const _getMeasurements = jest.fn()
const getMeasurements = jest.fn()
const {instance, props} = setup({query: {...query, database: ''}})
instance._getMeasurements = _getMeasurements
instance.getMeasurements = getMeasurements
instance.componentDidUpdate(props)
expect(_getMeasurements).not.toHaveBeenCalled()
expect(getMeasurements).not.toHaveBeenCalled()
})
it('does not fire _getMeasurements if the database does not change and the sources are equal', () => {
const _getMeasurements = jest.fn()
it('does not fire getMeasurements if the database does not change and the sources are equal', () => {
const getMeasurements = jest.fn()
const {instance, props} = setup()
instance._getMeasurements = _getMeasurements
instance.getMeasurements = getMeasurements
instance.componentDidUpdate(props)
expect(_getMeasurements).not.toHaveBeenCalled()
expect(getMeasurements).not.toHaveBeenCalled()
})
it('it fires _getMeasurements if there is a change in database', () => {
const _getMeasurements = jest.fn()
it('it fires getMeasurements if there is a change in database', () => {
const getMeasurements = jest.fn()
const {instance, props} = setup()
instance._getMeasurements = _getMeasurements
instance.getMeasurements = getMeasurements
instance.componentDidUpdate({
...props,
query: {...query, database: 'diffDb'},
})
expect(_getMeasurements).toHaveBeenCalled()
expect(getMeasurements).toHaveBeenCalled()
})
it('it calls _getMeasurements if there is a change in source', () => {
const _getMeasurements = jest.fn()
it('it calls getMeasurements if there is a change in source', () => {
const getMeasurements = jest.fn()
const {instance, props} = setup()
instance._getMeasurements = _getMeasurements
instance.getMeasurements = getMeasurements
instance.componentDidUpdate({
...props,
querySource: {...source, id: 'newSource'},
})
expect(_getMeasurements).toHaveBeenCalled()
expect(getMeasurements).toHaveBeenCalled()
})
})
})