Co-authored-by: Chris Henn <chris.henn@influxdata.com>
Co-authored-by: Andrew Watkins <andrew.watkinz@gmail.com>
pull/10616/head
Christopher Henn 2018-05-25 17:08:10 -07:00 committed by Andrew Watkins
parent c575a09d89
commit d9942259db
11 changed files with 213 additions and 135 deletions

View File

@ -1,9 +1,9 @@
import React, {PureComponent} from 'react'
import classnames from 'classnames'
import {measurements as measurementsAsync} from 'src/shared/apis/v2/metaQueries'
import parseMeasurements from 'src/shared/parsing/v2/measurements'
import MeasurementList from 'src/ifql/components/MeasurementList'
import {tags as fetchTags} from 'src/shared/apis/v2/metaQueries'
import parseTags from 'src/shared/parsing/v2/tags'
import TagList from 'src/ifql/components/TagList'
import {Service} from 'src/types'
interface Props {
@ -13,7 +13,7 @@ interface Props {
interface State {
isOpen: boolean
measurements: string[]
tags: string[]
}
class DatabaseListItem extends PureComponent<Props, State> {
@ -21,7 +21,7 @@ class DatabaseListItem extends PureComponent<Props, State> {
super(props)
this.state = {
isOpen: false,
measurements: [],
tags: [],
}
}
@ -29,17 +29,17 @@ class DatabaseListItem extends PureComponent<Props, State> {
const {db, service} = this.props
try {
const response = await measurementsAsync(service, db)
const measurements = parseMeasurements(response)
this.setState({measurements})
const response = await fetchTags(service, db)
const tags = parseTags(response)
this.setState({tags})
} catch (error) {
console.error(error)
}
}
public render() {
const {db} = this.props
const {measurements} = this.state
const {db, service} = this.props
const {tags} = this.state
return (
<div className={this.className} onClick={this.handleChooseDatabase}>
@ -48,7 +48,11 @@ class DatabaseListItem extends PureComponent<Props, State> {
{db}
<span className="ifql-schema-type">Bucket</span>
</div>
{this.state.isOpen && <MeasurementList measurements={measurements} />}
{this.state.isOpen && (
<>
<TagList db={db} service={service} tags={tags} filter={[]} />
</>
)}
</div>
)
}

View File

@ -1,17 +0,0 @@
import React, {PureComponent} from 'react'
import MeasurementListItem from 'src/ifql/components/MeasurementListItem'
interface Props {
measurements: string[]
}
export default class MeasurementList extends PureComponent<Props> {
public render() {
const {measurements} = this.props
return measurements.map(m => (
<MeasurementListItem key={m} measurement={m} />
))
}
}

View File

@ -1,30 +1,23 @@
import React, {PureComponent, MouseEvent} from 'react'
interface Props {
measurement: string
schemaType: string
name: string
}
interface State {
isOpen: boolean
}
export default class MeasurementListItem extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
isOpen: false,
}
}
export default class SchemaItem extends PureComponent<Props, State> {
public render() {
const {measurement} = this.props
const {schemaType} = this.props
return (
<div className={this.className}>
<div className="ifql-schema-item" onClick={this.handleClick}>
<div className="ifql-schema-item-toggle" />
{measurement}
<span className="ifql-schema-type">Measurement</span>
{name}
<span className="ifql-schema-type">{schemaType}</span>
</div>
</div>
)

View File

@ -1,65 +1,31 @@
import PropTypes from 'prop-types'
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {SchemaFilter, Service} from 'src/types'
import TagListItem from 'src/ifql/components/TagListItem'
import {getTags, getTagValues} from 'src/ifql/apis'
import {ErrorHandling} from 'src/shared/decorators/errors'
const {shape} = PropTypes
interface Props {
db: string
service: Service
tags: string[]
filter: SchemaFilter[]
}
interface State {
tags: {}
selectedTag: string
isOpen: boolean
}
@ErrorHandling
class TagList extends PureComponent<Props, State> {
public static contextTypes = {
source: shape({
links: shape({}).isRequired,
}).isRequired,
}
export default class TagList extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
tags: {},
selectedTag: '',
}
}
public componentDidMount() {
const {db} = this.props
if (!db) {
return
}
this.getTags()
}
public async getTags() {
const keys = await getTags()
const values = await getTagValues()
const tags = keys.reduce((acc, k) => {
return {...acc, [k]: values}
}, {})
this.setState({tags})
this.state = {isOpen: false}
}
public render() {
return _.map(this.state.tags, (tagValues: string[], tagKey: string) => (
<TagListItem key={tagKey} tagKey={tagKey} tagValues={tagValues} />
const {db, service, tags, filter} = this.props
return tags.map(t => (
<TagListItem key={t} db={db} tag={t} service={service} filter={filter} />
))
}
}
export default TagList

View File

@ -1,69 +1,95 @@
import classnames from 'classnames'
import React, {PureComponent, MouseEvent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import React, {PureComponent, CSSProperties, MouseEvent} from 'react'
import {Service, SchemaFilter} from 'src/types'
import {tagsFromMeasurement} from 'src/shared/apis/v2/metaQueries'
import parseTags from 'src/shared/parsing/v2/tags'
import TagList from 'src/ifql/components/TagList'
interface Props {
tagKey: string
tagValues: string[]
tag: string
db: string
service: Service
filter: SchemaFilter[]
}
interface State {
isOpen: boolean
loading: string
tags: string[]
}
@ErrorHandling
class TagListItem extends PureComponent<Props, State> {
enum RemoteDataState {
NotStarted = 'NotStarted',
Loading = 'Loading',
Done = 'Done',
Error = 'Error',
}
export default class TagListItem extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
isOpen: false,
loading: RemoteDataState.NotStarted,
tags: [],
}
}
public render() {
const {isOpen} = this.state
const {tag, db, service} = this.props
const {tags} = this.state
return (
<div className={this.className}>
<div className={this.className} style={this.style}>
<div className="ifql-schema-item" onClick={this.handleClick}>
<div className="ifql-schema-item-toggle" />
{this.tagItemLabel}
<span className="ifql-schema-type">Tag Key</span>
{tag}
<TagList db={db} service={service} tags={tags} />
</div>
{isOpen && this.renderTagValues}
</div>
)
}
private handleClick = (e: MouseEvent<HTMLElement>): void => {
private async getTags() {
const {db, service, tag, filter} = this.props
try {
const response = await tagsFromMeasurement(service, db, measurement)
const tags = parseTags(response)
this.setState({
tags,
loading: RemoteDataState.Done,
})
} catch (error) {
console.error(error)
}
}
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (this.isFetchable) {
this.getTags()
}
this.setState({isOpen: !this.state.isOpen})
}
private get tagItemLabel(): string {
const {tagKey} = this.props
return `${tagKey}`
}
private get isFetchable(): boolean {
const {isOpen, loading} = this.state
private get renderTagValues(): JSX.Element[] | JSX.Element {
const {tagValues} = this.props
if (!tagValues || !tagValues.length) {
return <div className="ifql-schema-tree__empty">No tag values</div>
}
return tagValues.map(v => {
return (
<div key={v} className="ifql-schema-item readonly ifql-tree-node">
{v}
</div>
)
})
return (
!isOpen &&
(loading === RemoteDataState.NotStarted ||
loading !== RemoteDataState.Error)
)
}
private get className(): string {
const {isOpen} = this.state
return classnames('ifql-schema-tree ifql-tree-node', {expanded: isOpen})
const openClass = isOpen ? 'expanded' : ''
return `ifql-schema-tree ifql-tree-node ${openClass}`
}
}
export default TagListItem

View File

@ -3,33 +3,61 @@ import _ from 'lodash'
import AJAX from 'src/utils/ajax'
import {Service} from 'src/types'
const sendIFQLRequest = (service: Service, script: string) => {
const and = encodeURIComponent('&')
const mark = encodeURIComponent('?')
const garbage = script.replace(/\s/g, '') // server cannot handle whitespace
return AJAX({
method: 'POST',
url: `${
service.links.proxy
}?path=/v1/query${mark}orgName=defaulorgname${and}q=${garbage}`,
})
}
export const measurements = async (
service: Service,
db: string
): Promise<any> => {
const script = `
from(db:"${db}")
|> range(start:-1h)
|> range(start:-24h)
|> group(by:["_measurement"])
|> distinct(column:"_measurement")
|> group()
`
return proxy(service, script)
}
export const tags = async (service: Service, db: string): Promise<any> => {
const script = `
from(db: "${db}")
|> range(start: -24h)
|> group(none: true)
|> keys(except:["_time","_value","_start","_stop"])
|> map(fn: (r) => r._value)
`
return proxy(service, script)
}
export const tagsFromMeasurement = async (
service: Service,
db: string,
measurement: string
): Promise<any> => {
const script = `
from(db:"${db}")
|> range(start:-24h)
|> filter(fn:(r) => r._measurement == "${measurement}")
|> group()
|> keys(except:["_time","_value","_start","_stop"])
`
return proxy(service, script)
}
const proxy = async (service: Service, script: string) => {
const and = encodeURIComponent('&')
const mark = encodeURIComponent('?')
const garbage = script.replace(/\s/g, '') // server cannot handle whitespace
try {
const response = await sendIFQLRequest(service, script)
const response = await AJAX({
method: 'POST',
url: `${
service.links.proxy
}?path=/v1/query${mark}orgName=defaulorgname${and}q=${garbage}`,
})
return response.data
} catch (error) {

View File

@ -0,0 +1,23 @@
import _ from 'lodash'
import {ScriptResult} from 'src/types'
import {parseResults} from 'src/shared/parsing/v2/results'
const parseTags = (resp: string): string[] => {
const results = parseResults(resp)
if (results.length === 0) {
return []
}
const tags = results.reduce<string[]>((acc, result: ScriptResult) => {
const colIndex = result.data[0].findIndex(header => header === '_value')
const resultTags = result.data.slice(1).map(row => row[colIndex])
return [...acc, ...resultTags]
}, [])
return _.sortBy(tags, t => t.toLocaleLowerCase())
}
export default parseTags

View File

@ -119,3 +119,8 @@ export interface ScriptResult {
data: string[][]
metadata: string[][]
}
export interface SchemaFilter {
key: string
value: string
}

View File

@ -22,7 +22,7 @@ import {AlertRule, Kapacitor, Task} from './kapacitor'
import {Source, SourceLinks} from './sources'
import {DropdownAction, DropdownItem} from './shared'
import {Notification, NotificationFunc} from './notifications'
import {ScriptResult, ScriptStatus} from './ifql'
import {ScriptResult, ScriptStatus, SchemaFilter} from './ifql'
export {
Me,
@ -64,4 +64,5 @@ export {
LayoutQuery,
ScriptResult,
ScriptStatus,
SchemaFilter,
}

View File

@ -67,6 +67,30 @@ export const MEASUREMENTS_RESPONSE = `#datatype,string,long,dateTime:RFC3339,dat
`
/*
From the following request:
from(db: "telegraf")
|> range(start: -24h)
|> group(none: true)
|> keys(except:["_time","_value","_start","_stop"])
|> map(fn: (r) => r._value)
*/
export const TAGS_RESPONSE = `#datatype,string,long,string
#partition,false,false,false
#default,_result,,
,result,table,_value
,,0,_field
,,0,_measurement
,,0,cpu
,,0,device
,,0,fstype
,,0,host
,,0,mode
,,0,name
,,0,path
`
// prettier-ignore
export const LARGE_RESPONSE = `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
#partition,false,false,false,false,false,false,true,true,true,true

View File

@ -0,0 +1,25 @@
import parseTags from 'src/shared/parsing/v2/tags'
import {TAGS_RESPONSE} from 'test/shared/parsing/v2/constants'
describe('measurements parser', () => {
it('returns no measurements for an empty results response', () => {
expect(parseTags('')).toEqual([])
})
it('returns the approriate measurements', () => {
const actual = parseTags(TAGS_RESPONSE)
const expected = [
'_field',
'_measurement',
'cpu',
'device',
'fstype',
'host',
'mode',
'name',
'path',
]
expect(actual).toEqual(expected)
})
})