Merge pull request #4574 from influxdata/flux/se-reorganize
Reorganize Flux Schema Explorerpull/4590/head
commit
62b5b1cdf5
|
@ -1,16 +1,14 @@
|
|||
import React, {PureComponent, ChangeEvent, MouseEvent} from 'react'
|
||||
import React, {PureComponent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {CopyToClipboard} from 'react-copy-to-clipboard'
|
||||
|
||||
import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
|
||||
import parseValuesColumn from 'src/shared/parsing/flux/values'
|
||||
import TagList from 'src/flux/components/TagList'
|
||||
import {
|
||||
notifyCopyToClipboardSuccess,
|
||||
notifyCopyToClipboardFailed,
|
||||
} from 'src/shared/copy/notifications'
|
||||
|
||||
import {Source, NotificationAction} from 'src/types'
|
||||
import SchemaItemCategories from 'src/flux/components/SchemaItemCategories'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
|
@ -20,7 +18,6 @@ interface Props {
|
|||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
tags: string[]
|
||||
searchTerm: string
|
||||
}
|
||||
|
||||
|
@ -29,23 +26,10 @@ class DatabaseListItem extends PureComponent<Props, State> {
|
|||
super(props)
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
tags: [],
|
||||
searchTerm: '',
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
const {db, source} = this.props
|
||||
|
||||
try {
|
||||
const response = await fetchTagKeys(source, db, [])
|
||||
const tags = parseValuesColumn(response)
|
||||
this.setState({tags})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {db} = this.props
|
||||
|
||||
|
@ -64,15 +48,20 @@ class DatabaseListItem extends PureComponent<Props, State> {
|
|||
</div>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{this.filterAndTagList}
|
||||
{this.categories}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get tags(): string[] {
|
||||
const {tags, searchTerm} = this.state
|
||||
const term = searchTerm.toLocaleLowerCase()
|
||||
return tags.filter(t => t.toLocaleLowerCase().includes(term))
|
||||
private get categories(): JSX.Element {
|
||||
const {db, source, notify} = this.props
|
||||
const {isOpen} = this.state
|
||||
|
||||
return (
|
||||
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
|
||||
<SchemaItemCategories db={db} source={source} notify={notify} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
|
@ -81,35 +70,6 @@ class DatabaseListItem extends PureComponent<Props, State> {
|
|||
})
|
||||
}
|
||||
|
||||
private get filterAndTagList(): JSX.Element {
|
||||
const {db, source, notify} = this.props
|
||||
const {isOpen, searchTerm} = this.state
|
||||
|
||||
return (
|
||||
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${db}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onClick={this.handleInputClick}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
</div>
|
||||
<TagList
|
||||
db={db}
|
||||
source={source}
|
||||
tags={this.tags}
|
||||
filter={[]}
|
||||
notify={notify}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClickCopy = e => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
@ -126,19 +86,9 @@ class DatabaseListItem extends PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
private onSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
searchTerm: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
private handleClick = () => {
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
|
||||
private handleInputClick = (e: MouseEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
export default DatabaseListItem
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
// Libraries
|
||||
import React, {PureComponent, ChangeEvent, MouseEvent} from 'react'
|
||||
import {CopyToClipboard} from 'react-copy-to-clipboard'
|
||||
|
||||
// Components
|
||||
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
|
||||
|
||||
// APIS
|
||||
import {fields as fetchFields} from 'src/shared/apis/flux/metaQueries'
|
||||
|
||||
// Utils
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import parseValuesColumn from 'src/shared/parsing/flux/values'
|
||||
import {
|
||||
notifyCopyToClipboardSuccess,
|
||||
notifyCopyToClipboardFailed,
|
||||
} from 'src/shared/copy/notifications'
|
||||
|
||||
// types
|
||||
import {Source, NotificationAction, RemoteDataState} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
source: Source
|
||||
tag?: {key: string; value: string}
|
||||
measurement?: string
|
||||
notify: NotificationAction
|
||||
}
|
||||
|
||||
interface State {
|
||||
fields: string[]
|
||||
searchTerm: string
|
||||
loading: RemoteDataState
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class FieldList extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
fields: [],
|
||||
searchTerm: '',
|
||||
loading: RemoteDataState.NotStarted,
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
this.setState({loading: RemoteDataState.Loading})
|
||||
try {
|
||||
const fields = await this.fetchFields()
|
||||
this.setState({fields, loading: RemoteDataState.Done})
|
||||
} catch (error) {
|
||||
this.setState({loading: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {tag} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${(tag && tag.value) || 'Fields'}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onClick={this.handleInputClick}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
</div>
|
||||
{this.fields}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private get fields(): JSX.Element | JSX.Element[] {
|
||||
const {searchTerm, loading} = this.state
|
||||
|
||||
if (loading === RemoteDataState.Loading) {
|
||||
return <LoaderSkeleton />
|
||||
}
|
||||
|
||||
const term = searchTerm.toLocaleLowerCase()
|
||||
const fields = this.state.fields.filter(f =>
|
||||
f.toLocaleLowerCase().includes(term)
|
||||
)
|
||||
|
||||
if (fields.length) {
|
||||
return fields.map(field => (
|
||||
<div
|
||||
className="flux-schema-tree flux-schema--child"
|
||||
key={field}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<div className="flux-schema--item">
|
||||
<div className="flex-schema-item-group">
|
||||
{field}
|
||||
<span className="flux-schema--type">Field</span>
|
||||
</div>
|
||||
<CopyToClipboard text={field} onCopy={this.handleCopyAttempt}>
|
||||
<div className="flux-schema-copy" onClick={this.handleClick}>
|
||||
<span className="icon duplicate" title="copy to clipboard" />
|
||||
Copy
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flux-schema-tree flux-schema--child">
|
||||
<div className="flux-schema--item no-hover" onClick={this.handleClick}>
|
||||
<div className="no-results">No more fields.</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private async fetchFields(): Promise<string[]> {
|
||||
const {source, db} = this.props
|
||||
|
||||
const filter = this.filters
|
||||
const limit = 50
|
||||
|
||||
const response = await fetchFields(source, db, filter, limit)
|
||||
const fields = parseValuesColumn(response)
|
||||
return fields
|
||||
}
|
||||
|
||||
private get filters(): Array<{value: string; key: string}> {
|
||||
const {tag, measurement} = this.props
|
||||
const filters = []
|
||||
if (tag) {
|
||||
filters.push(tag)
|
||||
}
|
||||
if (measurement) {
|
||||
filters.push({key: '_measurement', value: measurement})
|
||||
}
|
||||
|
||||
return filters
|
||||
}
|
||||
|
||||
private handleClick = (e): void => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
private handleCopyAttempt = (
|
||||
copiedText: string,
|
||||
isSuccessful: boolean
|
||||
): void => {
|
||||
const {notify} = this.props
|
||||
if (isSuccessful) {
|
||||
notify(notifyCopyToClipboardSuccess(copiedText))
|
||||
} else {
|
||||
notify(notifyCopyToClipboardFailed(copiedText))
|
||||
}
|
||||
}
|
||||
|
||||
private onSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
searchTerm: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
private handleInputClick = (e: MouseEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
export default FieldList
|
|
@ -0,0 +1,125 @@
|
|||
// Libraries
|
||||
import React, {PureComponent, MouseEvent, ChangeEvent} from 'react'
|
||||
|
||||
// Components
|
||||
import MeasurementListItem from 'src/flux/components/MeasurementListItem'
|
||||
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
|
||||
|
||||
// apis
|
||||
import {measurements as fetchMeasurements} from 'src/shared/apis/flux/metaQueries'
|
||||
|
||||
// Utils
|
||||
import parseValuesColumn from 'src/shared/parsing/flux/values'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// types
|
||||
import {Source, NotificationAction, RemoteDataState} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
source: Source
|
||||
notify: NotificationAction
|
||||
}
|
||||
|
||||
interface State {
|
||||
searchTerm: string
|
||||
measurements: string[]
|
||||
loading: RemoteDataState
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TagValueList extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
measurements: [],
|
||||
searchTerm: '',
|
||||
loading: RemoteDataState.NotStarted,
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
this.setState({loading: RemoteDataState.Loading})
|
||||
try {
|
||||
const measurements = await this.fetchMeasurements()
|
||||
this.setState({measurements, loading: RemoteDataState.Done})
|
||||
} catch (error) {
|
||||
this.setState({loading: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {searchTerm} = this.state
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder="Filter within Measurements"
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onClick={this.handleClick}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
</div>
|
||||
{this.measurements}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private get measurements(): JSX.Element | JSX.Element[] {
|
||||
const {source, db, notify} = this.props
|
||||
const {searchTerm, loading} = this.state
|
||||
|
||||
if (loading === RemoteDataState.Loading) {
|
||||
return <LoaderSkeleton />
|
||||
}
|
||||
const term = searchTerm.toLocaleLowerCase()
|
||||
const measurements = this.state.measurements.filter(m =>
|
||||
m.toLocaleLowerCase().includes(term)
|
||||
)
|
||||
if (measurements.length) {
|
||||
return measurements.map(measurement => (
|
||||
<MeasurementListItem
|
||||
source={source}
|
||||
db={db}
|
||||
searchTerm={searchTerm}
|
||||
measurement={measurement}
|
||||
key={measurement}
|
||||
notify={notify}
|
||||
/>
|
||||
))
|
||||
}
|
||||
return (
|
||||
<div className="flux-schema-tree flux-schema--child">
|
||||
<div className="flux-schema--item no-hover" onClick={this.handleClick}>
|
||||
<div className="no-results">No more measurements.</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private async fetchMeasurements(): Promise<string[]> {
|
||||
const {source, db} = this.props
|
||||
|
||||
const response = await fetchMeasurements(source, db)
|
||||
const measurements = parseValuesColumn(response)
|
||||
return measurements
|
||||
}
|
||||
|
||||
private onSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
searchTerm: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
export default TagValueList
|
|
@ -0,0 +1,113 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
import {CopyToClipboard} from 'react-copy-to-clipboard'
|
||||
|
||||
// Components
|
||||
import TagKeyList from 'src/flux/components/TagKeyList'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// Utils
|
||||
import {
|
||||
notifyCopyToClipboardSuccess,
|
||||
notifyCopyToClipboardFailed,
|
||||
} from 'src/shared/copy/notifications'
|
||||
|
||||
// Constants
|
||||
import {OpenState} from 'src/flux/constants/explorer'
|
||||
|
||||
// types
|
||||
import {Source, NotificationAction} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
source: Source
|
||||
searchTerm: string
|
||||
measurement: string
|
||||
notify: NotificationAction
|
||||
}
|
||||
|
||||
interface State {
|
||||
opened: OpenState
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class MeasurementListItem extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
opened: OpenState.UNOPENED,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {db, source, measurement, notify} = this.props
|
||||
const {opened} = this.state
|
||||
const isOpen = opened === OpenState.OPENED
|
||||
const isUnopen = opened === OpenState.UNOPENED
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flux-schema-tree flux-schema--child ${
|
||||
isOpen ? 'expanded' : ''
|
||||
}`}
|
||||
key={measurement}
|
||||
onClick={this.handleItemClick}
|
||||
>
|
||||
<div className="flux-schema--item">
|
||||
<div className="flex-schema-item-group">
|
||||
<div className="flux-schema--expander" />
|
||||
{measurement}
|
||||
<span className="flux-schema--type">Measurement</span>
|
||||
</div>
|
||||
<CopyToClipboard text={measurement} onCopy={this.handleCopyAttempt}>
|
||||
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
|
||||
<span className="icon duplicate" title="copy to clipboard" />
|
||||
Copy
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{!isUnopen && (
|
||||
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
|
||||
<TagKeyList
|
||||
db={db}
|
||||
source={source}
|
||||
notify={notify}
|
||||
measurement={measurement}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClickCopy = (e): void => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
private handleCopyAttempt = (
|
||||
copiedText: string,
|
||||
isSuccessful: boolean
|
||||
): void => {
|
||||
const {notify} = this.props
|
||||
if (isSuccessful) {
|
||||
notify(notifyCopyToClipboardSuccess(copiedText))
|
||||
} else {
|
||||
notify(notifyCopyToClipboardFailed(copiedText))
|
||||
}
|
||||
}
|
||||
|
||||
private handleItemClick = (e): void => {
|
||||
e.stopPropagation()
|
||||
|
||||
const opened = this.state.opened
|
||||
|
||||
if (opened === OpenState.OPENED) {
|
||||
this.setState({opened: OpenState.ClOSED})
|
||||
return
|
||||
}
|
||||
this.setState({opened: OpenState.OPENED})
|
||||
}
|
||||
}
|
||||
|
||||
export default MeasurementListItem
|
|
@ -0,0 +1,49 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
|
||||
// Components
|
||||
import SchemaItemCategory, {
|
||||
CategoryType,
|
||||
} from 'src/flux/components/SchemaItemCategory'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// Types
|
||||
import {Source, NotificationAction} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
db: string
|
||||
notify: NotificationAction
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class SchemaItemCategories extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {source, db, notify} = this.props
|
||||
|
||||
return (
|
||||
<>
|
||||
<SchemaItemCategory
|
||||
source={source}
|
||||
db={db}
|
||||
type={CategoryType.Measurements}
|
||||
notify={notify}
|
||||
/>
|
||||
<SchemaItemCategory
|
||||
source={source}
|
||||
db={db}
|
||||
type={CategoryType.Tags}
|
||||
notify={notify}
|
||||
/>
|
||||
<SchemaItemCategory
|
||||
source={source}
|
||||
db={db}
|
||||
type={CategoryType.Fields}
|
||||
notify={notify}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SchemaItemCategories
|
|
@ -0,0 +1,106 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
|
||||
// Components
|
||||
import MeasurementList from 'src/flux/components/MeasurementList'
|
||||
import FieldList from 'src/flux/components/FieldList'
|
||||
import TagKeyList from 'src/flux/components/TagKeyList'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// Constants
|
||||
import {OpenState} from 'src/flux/constants/explorer'
|
||||
|
||||
// Types
|
||||
import {Source, NotificationAction} from 'src/types'
|
||||
|
||||
export enum CategoryType {
|
||||
Measurements = 'measurements',
|
||||
Fields = 'fields',
|
||||
Tags = 'tags',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
notify: NotificationAction
|
||||
db: string
|
||||
type: CategoryType
|
||||
}
|
||||
|
||||
interface State {
|
||||
opened: OpenState
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class SchemaItemCategory extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
opened: OpenState.UNOPENED,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {opened} = this.state
|
||||
const isOpen = opened === OpenState.OPENED
|
||||
const isUnopened = opened === OpenState.UNOPENED
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flux-schema-tree flux-schema--child ${
|
||||
isOpen ? 'expanded' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flux-schema--item" onClick={this.handleClick}>
|
||||
<div className="flex-schema-item-group">
|
||||
<div className="flux-schema--expander" />
|
||||
{this.categoryName}
|
||||
</div>
|
||||
</div>
|
||||
{!isUnopened && (
|
||||
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
|
||||
{this.itemList}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get categoryName(): string {
|
||||
switch (this.props.type) {
|
||||
case CategoryType.Measurements:
|
||||
return 'MEASURMENTS'
|
||||
case CategoryType.Fields:
|
||||
return 'FIELDS'
|
||||
case CategoryType.Tags:
|
||||
return 'TAGS'
|
||||
}
|
||||
}
|
||||
|
||||
private get itemList(): JSX.Element {
|
||||
const {type, db, source, notify} = this.props
|
||||
|
||||
switch (type) {
|
||||
case CategoryType.Measurements:
|
||||
return <MeasurementList db={db} source={source} notify={notify} />
|
||||
|
||||
case CategoryType.Fields:
|
||||
return <FieldList db={db} source={source} notify={notify} />
|
||||
case CategoryType.Tags:
|
||||
return <TagKeyList db={db} source={source} notify={notify} />
|
||||
}
|
||||
}
|
||||
|
||||
private handleClick = e => {
|
||||
e.stopPropagation()
|
||||
const opened = this.state.opened
|
||||
|
||||
if (opened === OpenState.OPENED) {
|
||||
this.setState({opened: OpenState.ClOSED})
|
||||
return
|
||||
}
|
||||
this.setState({opened: OpenState.OPENED})
|
||||
}
|
||||
}
|
||||
|
||||
export default SchemaItemCategory
|
|
@ -0,0 +1,134 @@
|
|||
// Libraries
|
||||
import React, {PureComponent, ChangeEvent, MouseEvent} from 'react'
|
||||
|
||||
// Components
|
||||
import TagKeyListItem from 'src/flux/components/TagKeyListItem'
|
||||
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
|
||||
|
||||
// apis
|
||||
import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
|
||||
|
||||
// Utils
|
||||
import parseValuesColumn from 'src/shared/parsing/flux/values'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// types
|
||||
import {Source, NotificationAction, RemoteDataState} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
measurement?: string
|
||||
source: Source
|
||||
notify: NotificationAction
|
||||
}
|
||||
|
||||
interface State {
|
||||
tagKeys: string[]
|
||||
searchTerm: string
|
||||
loading: RemoteDataState
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TagKeyList extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
tagKeys: [],
|
||||
searchTerm: '',
|
||||
loading: RemoteDataState.NotStarted,
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
this.setState({loading: RemoteDataState.Loading})
|
||||
try {
|
||||
const tagKeys = await this.fetchTagKeys()
|
||||
this.setState({tagKeys, loading: RemoteDataState.Done})
|
||||
} catch (error) {
|
||||
this.setState({loading: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {measurement} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${measurement || 'Tags'}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onClick={this.handleClick}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
</div>
|
||||
{this.tagKeys}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private get tagKeys(): JSX.Element | JSX.Element[] {
|
||||
const {db, source, notify, measurement} = this.props
|
||||
const {searchTerm, loading} = this.state
|
||||
|
||||
if (loading === RemoteDataState.Loading) {
|
||||
return <LoaderSkeleton />
|
||||
}
|
||||
|
||||
const excludedTagKeys = ['_measurement', '_field']
|
||||
const term = searchTerm.toLocaleLowerCase()
|
||||
const tagKeys = this.state.tagKeys.filter(
|
||||
tk =>
|
||||
!excludedTagKeys.includes(tk) && tk.toLocaleLowerCase().includes(term)
|
||||
)
|
||||
if (tagKeys.length) {
|
||||
return tagKeys.map(tagKey => (
|
||||
<TagKeyListItem
|
||||
db={db}
|
||||
source={source}
|
||||
searchTerm={searchTerm}
|
||||
tagKey={tagKey}
|
||||
measurement={measurement}
|
||||
key={tagKey}
|
||||
notify={notify}
|
||||
/>
|
||||
))
|
||||
}
|
||||
return (
|
||||
<div className="flux-schema-tree flux-schema--child">
|
||||
<div className="flux-schema--item no-hover" onClick={this.handleClick}>
|
||||
<div className="no-results">No more tag keys.</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private async fetchTagKeys(): Promise<string[]> {
|
||||
const {source, db, measurement} = this.props
|
||||
const filter = measurement
|
||||
? [{key: '_measurement', value: measurement}]
|
||||
: []
|
||||
|
||||
const response = await fetchTagKeys(source, db, filter)
|
||||
const tagKeys = parseValuesColumn(response)
|
||||
return tagKeys
|
||||
}
|
||||
|
||||
private onSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
searchTerm: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
export default TagKeyList
|
|
@ -0,0 +1,115 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
import {CopyToClipboard} from 'react-copy-to-clipboard'
|
||||
|
||||
// Components
|
||||
import TagValueList from 'src/flux/components/TagValueList'
|
||||
|
||||
// Utils
|
||||
import {
|
||||
notifyCopyToClipboardSuccess,
|
||||
notifyCopyToClipboardFailed,
|
||||
} from 'src/shared/copy/notifications'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// Constants
|
||||
import {OpenState} from 'src/flux/constants/explorer'
|
||||
|
||||
// types
|
||||
import {Source, NotificationAction} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
source: Source
|
||||
searchTerm: string
|
||||
tagKey: string
|
||||
measurement?: string
|
||||
notify: NotificationAction
|
||||
}
|
||||
|
||||
interface State {
|
||||
opened: OpenState
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TagKeyListItem extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
opened: OpenState.UNOPENED,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {db, source, tagKey, notify, measurement} = this.props
|
||||
const {opened} = this.state
|
||||
const isOpen = opened === OpenState.OPENED
|
||||
const isUnopen = opened === OpenState.UNOPENED
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flux-schema-tree flux-schema--child ${
|
||||
isOpen ? 'expanded' : ''
|
||||
}`}
|
||||
key={tagKey}
|
||||
onClick={this.handleItemClick}
|
||||
>
|
||||
<div className="flux-schema--item">
|
||||
<div className="flex-schema-item-group">
|
||||
<div className="flux-schema--expander" />
|
||||
{tagKey}
|
||||
<span className="flux-schema--type">Tag Key</span>
|
||||
</div>
|
||||
<CopyToClipboard text={tagKey} onCopy={this.handleCopyAttempt}>
|
||||
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
|
||||
<span className="icon duplicate" title="copy to clipboard" />
|
||||
Copy
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{!isUnopen && (
|
||||
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
|
||||
<TagValueList
|
||||
db={db}
|
||||
source={source}
|
||||
tagKey={tagKey}
|
||||
measurement={measurement}
|
||||
notify={notify}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClickCopy = (e): void => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
private handleCopyAttempt = (
|
||||
copiedText: string,
|
||||
isSuccessful: boolean
|
||||
): void => {
|
||||
const {notify} = this.props
|
||||
if (isSuccessful) {
|
||||
notify(notifyCopyToClipboardSuccess(copiedText))
|
||||
} else {
|
||||
notify(notifyCopyToClipboardFailed(copiedText))
|
||||
}
|
||||
}
|
||||
|
||||
private handleItemClick = (e): void => {
|
||||
e.stopPropagation()
|
||||
|
||||
const opened = this.state.opened
|
||||
|
||||
if (opened === OpenState.OPENED) {
|
||||
this.setState({opened: OpenState.ClOSED})
|
||||
return
|
||||
}
|
||||
this.setState({opened: OpenState.OPENED})
|
||||
}
|
||||
}
|
||||
|
||||
export default TagKeyListItem
|
|
@ -1,48 +0,0 @@
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
|
||||
import TagListItem from 'src/flux/components/TagListItem'
|
||||
|
||||
import {SchemaFilter, Source, NotificationAction} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
source: Source
|
||||
tags: string[]
|
||||
filter: SchemaFilter[]
|
||||
notify: NotificationAction
|
||||
}
|
||||
|
||||
export default class TagList extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {db, source, tags, filter, notify} = this.props
|
||||
|
||||
if (tags.length) {
|
||||
return (
|
||||
<>
|
||||
{tags.map(t => (
|
||||
<TagListItem
|
||||
db={db}
|
||||
key={t}
|
||||
tagKey={t}
|
||||
source={source}
|
||||
filter={filter}
|
||||
notify={notify}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flux-schema-tree flux-schema--child">
|
||||
<div className="flux-schema--item no-hover" onClick={this.handleClick}>
|
||||
<div className="no-results">No more tag keys.</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick(e: MouseEvent<HTMLDivElement>) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
|
@ -1,327 +0,0 @@
|
|||
import React, {
|
||||
PureComponent,
|
||||
CSSProperties,
|
||||
ChangeEvent,
|
||||
MouseEvent,
|
||||
} from 'react'
|
||||
|
||||
import _ from 'lodash'
|
||||
import {CopyToClipboard} from 'react-copy-to-clipboard'
|
||||
|
||||
import {Source, SchemaFilter, RemoteDataState} from 'src/types'
|
||||
import {tagValues as fetchTagValues} from 'src/shared/apis/flux/metaQueries'
|
||||
import {explorer} from 'src/flux/constants'
|
||||
import parseValuesColumn from 'src/shared/parsing/flux/values'
|
||||
import TagValueList from 'src/flux/components/TagValueList'
|
||||
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
|
||||
import LoadingSpinner from 'src/flux/components/LoadingSpinner'
|
||||
import {
|
||||
notifyCopyToClipboardSuccess,
|
||||
notifyCopyToClipboardFailed,
|
||||
} from 'src/shared/copy/notifications'
|
||||
|
||||
import {NotificationAction} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
tagKey: string
|
||||
db: string
|
||||
source: Source
|
||||
filter: SchemaFilter[]
|
||||
notify: NotificationAction
|
||||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
loadingAll: RemoteDataState
|
||||
loadingSearch: RemoteDataState
|
||||
loadingMore: RemoteDataState
|
||||
tagValues: string[]
|
||||
searchTerm: string
|
||||
limit: number
|
||||
count: number | null
|
||||
}
|
||||
|
||||
export default class TagListItem extends PureComponent<Props, State> {
|
||||
private debouncedOnSearch: () => void
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
loadingAll: RemoteDataState.NotStarted,
|
||||
loadingSearch: RemoteDataState.NotStarted,
|
||||
loadingMore: RemoteDataState.NotStarted,
|
||||
tagValues: [],
|
||||
count: null,
|
||||
searchTerm: '',
|
||||
limit: explorer.TAG_VALUES_LIMIT,
|
||||
}
|
||||
|
||||
this.debouncedOnSearch = _.debounce(() => {
|
||||
this.searchTagValues()
|
||||
this.getCount()
|
||||
}, 250)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {tagKey, db, source, filter, notify} = this.props
|
||||
const {
|
||||
tagValues,
|
||||
searchTerm,
|
||||
loadingMore,
|
||||
count,
|
||||
limit,
|
||||
isOpen,
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
<div className={this.className}>
|
||||
<div className="flux-schema--item" onClick={this.handleClick}>
|
||||
<div className="flex-schema-item-group">
|
||||
<div className="flux-schema--expander" />
|
||||
{tagKey}
|
||||
<span className="flux-schema--type">Tag Key</span>
|
||||
</div>
|
||||
<CopyToClipboard text={tagKey} onCopy={this.handleCopyAttempt}>
|
||||
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
|
||||
<span className="icon duplicate" title="copy to clipboard" />
|
||||
Copy
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
|
||||
<div className="flux-schema--header" onClick={this.handleInputClick}>
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${tagKey}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
{this.isSearching && <LoadingSpinner style={this.spinnerStyle} />}
|
||||
</div>
|
||||
{this.count}
|
||||
</div>
|
||||
{this.isLoading && <LoaderSkeleton />}
|
||||
{!this.isLoading && (
|
||||
<TagValueList
|
||||
db={db}
|
||||
notify={notify}
|
||||
source={source}
|
||||
values={tagValues}
|
||||
tagKey={tagKey}
|
||||
filter={filter}
|
||||
onLoadMoreValues={this.handleLoadMoreValues}
|
||||
isLoadingMoreValues={loadingMore === RemoteDataState.Loading}
|
||||
shouldShowMoreValues={limit < count}
|
||||
loadMoreCount={this.loadMoreCount}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get count(): JSX.Element {
|
||||
const {count} = this.state
|
||||
|
||||
if (!count) {
|
||||
return
|
||||
}
|
||||
|
||||
let pluralizer = 's'
|
||||
|
||||
if (count === 1) {
|
||||
pluralizer = ''
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flux-schema--count">{`${count} Tag Value${pluralizer}`}</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get spinnerStyle(): CSSProperties {
|
||||
return {
|
||||
position: 'absolute',
|
||||
right: '18px',
|
||||
top: '11px',
|
||||
}
|
||||
}
|
||||
|
||||
private get isSearching(): boolean {
|
||||
return this.state.loadingSearch === RemoteDataState.Loading
|
||||
}
|
||||
|
||||
private get isLoading(): boolean {
|
||||
return this.state.loadingAll === RemoteDataState.Loading
|
||||
}
|
||||
|
||||
private onSearch = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
const searchTerm = e.target.value
|
||||
|
||||
this.setState({searchTerm, loadingSearch: RemoteDataState.Loading}, () =>
|
||||
this.debouncedOnSearch()
|
||||
)
|
||||
}
|
||||
|
||||
private handleInputClick = (e: MouseEvent<HTMLDivElement>): void => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
private searchTagValues = async () => {
|
||||
try {
|
||||
const tagValues = await this.getTagValues()
|
||||
|
||||
this.setState({
|
||||
tagValues,
|
||||
loadingSearch: RemoteDataState.Done,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
this.setState({loadingSearch: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
private getAllTagValues = async () => {
|
||||
this.setState({loadingAll: RemoteDataState.Loading})
|
||||
|
||||
try {
|
||||
const tagValues = await this.getTagValues()
|
||||
|
||||
this.setState({
|
||||
tagValues,
|
||||
loadingAll: RemoteDataState.Done,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
this.setState({loadingAll: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
private getMoreTagValues = async () => {
|
||||
this.setState({loadingMore: RemoteDataState.Loading})
|
||||
|
||||
try {
|
||||
const tagValues = await this.getTagValues()
|
||||
|
||||
this.setState({
|
||||
tagValues,
|
||||
loadingMore: RemoteDataState.Done,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
this.setState({loadingMore: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
private getTagValues = async () => {
|
||||
const {db, source, tagKey, filter} = this.props
|
||||
const {searchTerm, limit} = this.state
|
||||
const response = await fetchTagValues({
|
||||
source,
|
||||
bucket: db,
|
||||
filter,
|
||||
tagKey,
|
||||
limit,
|
||||
searchTerm,
|
||||
})
|
||||
const tagValues = parseValuesColumn(response)
|
||||
return tagValues
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
|
||||
if (this.isFetchable) {
|
||||
this.getCount()
|
||||
this.getAllTagValues()
|
||||
}
|
||||
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
|
||||
private handleLoadMoreValues = (): void => {
|
||||
const {limit} = this.state
|
||||
|
||||
this.setState(
|
||||
{limit: limit + explorer.TAG_VALUES_LIMIT},
|
||||
this.getMoreTagValues
|
||||
)
|
||||
}
|
||||
|
||||
private handleClickCopy = e => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
private handleCopyAttempt = (
|
||||
copiedText: string,
|
||||
isSuccessful: boolean
|
||||
): void => {
|
||||
const {notify} = this.props
|
||||
if (isSuccessful) {
|
||||
notify(notifyCopyToClipboardSuccess(copiedText))
|
||||
} else {
|
||||
notify(notifyCopyToClipboardFailed(copiedText))
|
||||
}
|
||||
}
|
||||
|
||||
private async getCount() {
|
||||
const {source, db, filter, tagKey} = this.props
|
||||
const {limit, searchTerm} = this.state
|
||||
try {
|
||||
const response = await fetchTagValues({
|
||||
source,
|
||||
bucket: db,
|
||||
filter,
|
||||
tagKey,
|
||||
limit,
|
||||
searchTerm,
|
||||
count: true,
|
||||
})
|
||||
|
||||
const parsed = parseValuesColumn(response)
|
||||
|
||||
if (parsed.length !== 1) {
|
||||
// We expect to never reach this state; instead, the Flux server should
|
||||
// return a non-200 status code is handled earlier (after fetching).
|
||||
// This return guards against some unexpected behavior---the Flux server
|
||||
// returning a 200 status code but ALSO having an error in the CSV
|
||||
// response body
|
||||
return
|
||||
}
|
||||
|
||||
const count = Number(parsed[0])
|
||||
|
||||
this.setState({count})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
private get loadMoreCount(): number {
|
||||
const {count, limit} = this.state
|
||||
|
||||
return Math.min(Math.abs(count - limit), explorer.TAG_VALUES_LIMIT)
|
||||
}
|
||||
|
||||
private get isFetchable(): boolean {
|
||||
const {isOpen, loadingAll} = this.state
|
||||
|
||||
return (
|
||||
!isOpen &&
|
||||
(loadingAll === RemoteDataState.NotStarted ||
|
||||
loadingAll === RemoteDataState.Error)
|
||||
)
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {isOpen} = this.state
|
||||
const openClass = isOpen ? 'expanded' : ''
|
||||
|
||||
return `flux-schema-tree flux-schema--child ${openClass}`
|
||||
}
|
||||
}
|
|
@ -1,76 +1,142 @@
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
// Libraries
|
||||
import React, {PureComponent, ChangeEvent, MouseEvent} from 'react'
|
||||
|
||||
// Components
|
||||
import TagValueListItem from 'src/flux/components/TagValueListItem'
|
||||
import LoadingSpinner from 'src/flux/components/LoadingSpinner'
|
||||
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
|
||||
|
||||
import {Source, SchemaFilter, NotificationAction} from 'src/types'
|
||||
// apis
|
||||
import {tagValues as fetchTagValues} from 'src/shared/apis/flux/metaQueries'
|
||||
|
||||
// Utils
|
||||
import parseValuesColumn from 'src/shared/parsing/flux/values'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// types
|
||||
import {Source, NotificationAction, RemoteDataState} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
tagKey: string
|
||||
values: string[]
|
||||
source: Source
|
||||
loadMoreCount: number
|
||||
filter: SchemaFilter[]
|
||||
tagKey: string
|
||||
notify: NotificationAction
|
||||
isLoadingMoreValues: boolean
|
||||
onLoadMoreValues: () => void
|
||||
shouldShowMoreValues: boolean
|
||||
measurement: string
|
||||
}
|
||||
|
||||
export default class TagValueList extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {
|
||||
db,
|
||||
notify,
|
||||
source,
|
||||
values,
|
||||
tagKey,
|
||||
filter,
|
||||
shouldShowMoreValues,
|
||||
} = this.props
|
||||
interface State {
|
||||
tagValues: string[]
|
||||
searchTerm: string
|
||||
loading: RemoteDataState
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TagValueList extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
tagValues: [],
|
||||
searchTerm: '',
|
||||
loading: RemoteDataState.Loading,
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
this.setState({loading: RemoteDataState.Loading})
|
||||
try {
|
||||
const tagValues = await this.fetchTagValues()
|
||||
this.setState({tagValues, loading: RemoteDataState.Done})
|
||||
} catch (error) {
|
||||
this.setState({loading: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {tagKey} = this.props
|
||||
const {searchTerm} = this.state
|
||||
return (
|
||||
<>
|
||||
{values.map((v, i) => (
|
||||
<TagValueListItem
|
||||
key={i}
|
||||
db={db}
|
||||
value={v}
|
||||
tagKey={tagKey}
|
||||
source={source}
|
||||
filter={filter}
|
||||
notify={notify}
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${tagKey}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onClick={this.handleClick}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
))}
|
||||
{shouldShowMoreValues && (
|
||||
<div className="flux-schema-tree flux-schema--child">
|
||||
<div className="flux-schema--item no-hover">
|
||||
<button
|
||||
className="btn btn-xs btn-default increase-values-limit"
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{this.buttonValue}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{this.tagValues}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
this.props.onLoadMoreValues()
|
||||
}
|
||||
private get tagValues(): JSX.Element | JSX.Element[] {
|
||||
const {source, db, tagKey, measurement, notify} = this.props
|
||||
const {searchTerm, loading} = this.state
|
||||
|
||||
private get buttonValue(): string | JSX.Element {
|
||||
const {isLoadingMoreValues, loadMoreCount, tagKey} = this.props
|
||||
|
||||
if (isLoadingMoreValues) {
|
||||
return <LoadingSpinner />
|
||||
if (loading === RemoteDataState.Loading) {
|
||||
return <LoaderSkeleton />
|
||||
}
|
||||
|
||||
return `Load next ${loadMoreCount} values for ${tagKey}`
|
||||
const term = searchTerm.toLocaleLowerCase()
|
||||
const tagValues = this.state.tagValues.filter(
|
||||
tv => tv !== '' && tv.toLocaleLowerCase().includes(term)
|
||||
)
|
||||
|
||||
if (tagValues.length) {
|
||||
return tagValues.map(tagValue => (
|
||||
<TagValueListItem
|
||||
source={source}
|
||||
db={db}
|
||||
searchTerm={searchTerm}
|
||||
tagValue={tagValue}
|
||||
tagKey={tagKey}
|
||||
measurement={measurement}
|
||||
key={tagValue}
|
||||
notify={notify}
|
||||
/>
|
||||
))
|
||||
}
|
||||
return (
|
||||
<div className="flux-schema-tree flux-schema--child">
|
||||
<div className="flux-schema--item no-hover" onClick={this.handleClick}>
|
||||
<div className="no-results">No more tag values.</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private async fetchTagValues(): Promise<string[]> {
|
||||
const {source, db, tagKey, measurement} = this.props
|
||||
const limit = 50
|
||||
|
||||
const filter = measurement
|
||||
? [{key: '_measurement', value: measurement}]
|
||||
: []
|
||||
|
||||
const response = await fetchTagValues({
|
||||
source,
|
||||
bucket: db,
|
||||
tagKey,
|
||||
filter,
|
||||
limit,
|
||||
})
|
||||
const tagValues = parseValuesColumn(response)
|
||||
return tagValues
|
||||
}
|
||||
|
||||
private onSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
searchTerm: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
export default TagValueList
|
||||
|
|
|
@ -1,154 +1,104 @@
|
|||
import React, {PureComponent, MouseEvent, ChangeEvent} from 'react'
|
||||
|
||||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
import {CopyToClipboard} from 'react-copy-to-clipboard'
|
||||
|
||||
import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
|
||||
import parseValuesColumn from 'src/shared/parsing/flux/values'
|
||||
import TagList from 'src/flux/components/TagList'
|
||||
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
|
||||
// Components
|
||||
import FieldList from 'src/flux/components/FieldList'
|
||||
|
||||
// Utils
|
||||
import {
|
||||
notifyCopyToClipboardSuccess,
|
||||
notifyCopyToClipboardFailed,
|
||||
} from 'src/shared/copy/notifications'
|
||||
|
||||
import {
|
||||
Source,
|
||||
SchemaFilter,
|
||||
RemoteDataState,
|
||||
NotificationAction,
|
||||
} from 'src/types'
|
||||
// Constants
|
||||
import {OpenState} from 'src/flux/constants/explorer'
|
||||
|
||||
// types
|
||||
import {Source, NotificationAction} from 'src/types'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
source: Source
|
||||
searchTerm: string
|
||||
tagValue: string
|
||||
tagKey: string
|
||||
value: string
|
||||
filter: SchemaFilter[]
|
||||
notify: NotificationAction
|
||||
measurement: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
tags: string[]
|
||||
loading: RemoteDataState
|
||||
searchTerm: string
|
||||
opened: OpenState
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TagValueListItem extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
tags: [],
|
||||
loading: RemoteDataState.NotStarted,
|
||||
searchTerm: '',
|
||||
opened: OpenState.UNOPENED,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {db, source, value, notify} = this.props
|
||||
const {searchTerm, isOpen} = this.state
|
||||
const {db, source, tagValue, tagKey, notify, measurement} = this.props
|
||||
const {opened} = this.state
|
||||
const isOpen = opened === OpenState.OPENED
|
||||
const isUnopen = opened === OpenState.UNOPENED
|
||||
|
||||
const tag = {key: tagKey, value: tagValue}
|
||||
|
||||
return (
|
||||
<div className={this.className} onClick={this.handleClick}>
|
||||
<div
|
||||
className={`flux-schema-tree flux-schema--child ${
|
||||
isOpen ? 'expanded' : ''
|
||||
}`}
|
||||
key={tagValue}
|
||||
onClick={this.handleItemClick}
|
||||
>
|
||||
<div className="flux-schema--item">
|
||||
<div className="flex-schema-item-group">
|
||||
<div className="flux-schema--expander" />
|
||||
{value}
|
||||
{tagValue}
|
||||
<span className="flux-schema--type">Tag Value</span>
|
||||
</div>
|
||||
<CopyToClipboard text={value} onCopy={this.handleCopyAttempt}>
|
||||
<CopyToClipboard text={tagValue} onCopy={this.handleCopyAttempt}>
|
||||
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
|
||||
<span className="icon duplicate" title="copy to clipboard" />
|
||||
Copy
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
|
||||
{this.isLoading && <LoaderSkeleton />}
|
||||
{!this.isLoading && (
|
||||
<>
|
||||
{!!this.tags.length && (
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${value}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onClick={this.handleInputClick}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<TagList
|
||||
db={db}
|
||||
notify={notify}
|
||||
source={source}
|
||||
tags={this.tags}
|
||||
filter={this.filter}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!isUnopen && (
|
||||
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
|
||||
<FieldList
|
||||
db={db}
|
||||
source={source}
|
||||
tag={tag}
|
||||
notify={notify}
|
||||
measurement={measurement}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get isLoading(): boolean {
|
||||
return this.state.loading === RemoteDataState.Loading
|
||||
}
|
||||
|
||||
private get filter(): SchemaFilter[] {
|
||||
const {filter, tagKey, value} = this.props
|
||||
|
||||
return [...filter, {key: tagKey, value}]
|
||||
}
|
||||
|
||||
private get tags(): string[] {
|
||||
const {tags, searchTerm} = this.state
|
||||
const term = searchTerm.toLocaleLowerCase()
|
||||
return tags.filter(t => t.toLocaleLowerCase().includes(term))
|
||||
}
|
||||
|
||||
private async getTags() {
|
||||
const {db, source} = this.props
|
||||
|
||||
this.setState({loading: RemoteDataState.Loading})
|
||||
|
||||
try {
|
||||
const response = await fetchTagKeys(source, db, this.filter)
|
||||
const tags = parseValuesColumn(response)
|
||||
this.setState({tags, loading: RemoteDataState.Done})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {isOpen} = this.state
|
||||
const openClass = isOpen ? 'expanded' : ''
|
||||
|
||||
return `flux-schema-tree flux-schema--child ${openClass}`
|
||||
}
|
||||
|
||||
private handleInputClick = (e: MouseEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||
private handleItemClick = (e): void => {
|
||||
e.stopPropagation()
|
||||
|
||||
if (this.isFetchable) {
|
||||
this.getTags()
|
||||
}
|
||||
const opened = this.state.opened
|
||||
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
if (opened === OpenState.OPENED) {
|
||||
this.setState({opened: OpenState.ClOSED})
|
||||
return
|
||||
}
|
||||
this.setState({opened: OpenState.OPENED})
|
||||
}
|
||||
|
||||
private handleClickCopy = e => {
|
||||
private handleClickCopy = (e): void => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
|
@ -163,22 +113,6 @@ class TagValueListItem extends PureComponent<Props, State> {
|
|||
notify(notifyCopyToClipboardFailed(copiedText))
|
||||
}
|
||||
}
|
||||
|
||||
private onSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
searchTerm: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
private get isFetchable(): boolean {
|
||||
const {isOpen, loading} = this.state
|
||||
|
||||
return (
|
||||
!isOpen &&
|
||||
(loading === RemoteDataState.NotStarted ||
|
||||
loading === RemoteDataState.Error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default TagValueListItem
|
||||
|
|
|
@ -1 +1,7 @@
|
|||
export const TAG_VALUES_LIMIT = 10
|
||||
|
||||
export enum OpenState {
|
||||
UNOPENED = 'unopened',
|
||||
OPENED = 'opened',
|
||||
ClOSED = 'closed',
|
||||
}
|
||||
|
|
|
@ -12,12 +12,27 @@ export const measurements = async (
|
|||
|> range(start:-24h)
|
||||
|> group(by:["_measurement"])
|
||||
|> distinct(column:"_measurement")
|
||||
|> group()s
|
||||
|> group()
|
||||
`
|
||||
|
||||
return proxy(source, script)
|
||||
}
|
||||
|
||||
export const fields = async (
|
||||
source: Source,
|
||||
bucket: string,
|
||||
filter: SchemaFilter[],
|
||||
limit: number
|
||||
): Promise<any> => {
|
||||
return await tagValues({
|
||||
bucket,
|
||||
source,
|
||||
tagKey: '_field',
|
||||
limit,
|
||||
filter,
|
||||
})
|
||||
}
|
||||
|
||||
export const tagKeys = async (
|
||||
source: Source,
|
||||
bucket: string,
|
||||
|
|
Loading…
Reference in New Issue