WIP
Co-authored-by: Chris Henn <chris.henn@influxdata.com> Co-authored-by: Andrew Watkins <andrew.watkinz@gmail.com>pull/10616/head
parent
c575a09d89
commit
d9942259db
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
))
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
|
@ -119,3 +119,8 @@ export interface ScriptResult {
|
|||
data: string[][]
|
||||
metadata: string[][]
|
||||
}
|
||||
|
||||
export interface SchemaFilter {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue