Merge pull request #4574 from influxdata/flux/se-reorganize

Reorganize Flux Schema Explorer
pull/4590/head
Iris Scholten 2018-10-15 11:32:13 -07:00 committed by GitHub
commit 62b5b1cdf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1024 additions and 610 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()
}
}

View File

@ -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}`
}
}

View File

@ -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

View File

@ -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

View File

@ -1 +1,7 @@
export const TAG_VALUES_LIMIT = 10
export enum OpenState {
UNOPENED = 'unopened',
OPENED = 'opened',
ClOSED = 'closed',
}

View File

@ -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,