From 3ef3dfab1153eb46e75b93bd1b1e0223daab8f3a Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Thu, 11 Oct 2018 16:21:06 -0700 Subject: [PATCH] Move tags, fields, and measurements under category labels --- ui/src/flux/components/DatabaseListItem.tsx | 74 +--- ui/src/flux/components/FieldList.tsx | 176 ++++++++++ ui/src/flux/components/MeasurementList.tsx | 125 +++++++ .../flux/components/MeasurementListItem.tsx | 113 ++++++ .../flux/components/SchemaItemCategories.tsx | 49 +++ ui/src/flux/components/SchemaItemCategory.tsx | 106 ++++++ ui/src/flux/components/TagKeyList.tsx | 134 +++++++ ui/src/flux/components/TagKeyListItem.tsx | 115 ++++++ ui/src/flux/components/TagList.tsx | 48 --- ui/src/flux/components/TagListItem.tsx | 327 ------------------ ui/src/flux/components/TagValueList.tsx | 172 ++++++--- ui/src/flux/components/TagValueListItem.tsx | 172 +++------ ui/src/flux/constants/explorer.ts | 6 + ui/src/shared/apis/flux/metaQueries.ts | 17 +- 14 files changed, 1024 insertions(+), 610 deletions(-) create mode 100644 ui/src/flux/components/FieldList.tsx create mode 100644 ui/src/flux/components/MeasurementList.tsx create mode 100644 ui/src/flux/components/MeasurementListItem.tsx create mode 100644 ui/src/flux/components/SchemaItemCategories.tsx create mode 100644 ui/src/flux/components/SchemaItemCategory.tsx create mode 100644 ui/src/flux/components/TagKeyList.tsx create mode 100644 ui/src/flux/components/TagKeyListItem.tsx delete mode 100644 ui/src/flux/components/TagList.tsx delete mode 100644 ui/src/flux/components/TagListItem.tsx diff --git a/ui/src/flux/components/DatabaseListItem.tsx b/ui/src/flux/components/DatabaseListItem.tsx index 1538789fb..284e5bc98 100644 --- a/ui/src/flux/components/DatabaseListItem.tsx +++ b/ui/src/flux/components/DatabaseListItem.tsx @@ -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 { 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 { - {this.filterAndTagList} + {this.categories} ) } - 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 ( +
+ +
+ ) } private get className(): string { @@ -81,35 +70,6 @@ class DatabaseListItem extends PureComponent { }) } - private get filterAndTagList(): JSX.Element { - const {db, source, notify} = this.props - const {isOpen, searchTerm} = this.state - - return ( -
-
- -
- -
- ) - } - private handleClickCopy = e => { e.stopPropagation() } @@ -126,19 +86,9 @@ class DatabaseListItem extends PureComponent { } } - private onSearch = (e: ChangeEvent) => { - this.setState({ - searchTerm: e.target.value, - }) - } - private handleClick = () => { this.setState({isOpen: !this.state.isOpen}) } - - private handleInputClick = (e: MouseEvent) => { - e.stopPropagation() - } } export default DatabaseListItem diff --git a/ui/src/flux/components/FieldList.tsx b/ui/src/flux/components/FieldList.tsx new file mode 100644 index 000000000..28f52dddf --- /dev/null +++ b/ui/src/flux/components/FieldList.tsx @@ -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 { + 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 ( + <> +
+ +
+ {this.fields} + + ) + } + + private get fields(): JSX.Element | JSX.Element[] { + const {searchTerm, loading} = this.state + + if (loading === RemoteDataState.Loading) { + return + } + + const term = searchTerm.toLocaleLowerCase() + const fields = this.state.fields.filter(f => + f.toLocaleLowerCase().includes(term) + ) + + if (fields.length) { + return fields.map(field => ( +
+
+
+ {field} + Field +
+ +
+ + Copy +
+
+
+
+ )) + } + + return ( +
+
+
No more fields.
+
+
+ ) + } + + private async fetchFields(): Promise { + 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) => { + this.setState({ + searchTerm: e.target.value, + }) + } + + private handleInputClick = (e: MouseEvent) => { + e.stopPropagation() + } +} + +export default FieldList diff --git a/ui/src/flux/components/MeasurementList.tsx b/ui/src/flux/components/MeasurementList.tsx new file mode 100644 index 000000000..164f50bbd --- /dev/null +++ b/ui/src/flux/components/MeasurementList.tsx @@ -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 { + 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 ( + <> +
+ +
+ {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 + } + const term = searchTerm.toLocaleLowerCase() + const measurements = this.state.measurements.filter(m => + m.toLocaleLowerCase().includes(term) + ) + if (measurements.length) { + return measurements.map(measurement => ( + + )) + } + return ( +
+
+
No more measurements.
+
+
+ ) + } + + private async fetchMeasurements(): Promise { + const {source, db} = this.props + + const response = await fetchMeasurements(source, db) + const measurements = parseValuesColumn(response) + return measurements + } + + private onSearch = (e: ChangeEvent) => { + this.setState({ + searchTerm: e.target.value, + }) + } + + private handleClick = (e: MouseEvent) => { + e.stopPropagation() + } +} + +export default TagValueList diff --git a/ui/src/flux/components/MeasurementListItem.tsx b/ui/src/flux/components/MeasurementListItem.tsx new file mode 100644 index 000000000..918cfb5e9 --- /dev/null +++ b/ui/src/flux/components/MeasurementListItem.tsx @@ -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 { + 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 ( +
+
+
+
+ {measurement} + Measurement +
+ +
+ + Copy +
+
+
+ {!isUnopen && ( +
+ +
+ )} +
+ ) + } + + 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 diff --git a/ui/src/flux/components/SchemaItemCategories.tsx b/ui/src/flux/components/SchemaItemCategories.tsx new file mode 100644 index 000000000..498586527 --- /dev/null +++ b/ui/src/flux/components/SchemaItemCategories.tsx @@ -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 { + public render() { + const {source, db, notify} = this.props + + return ( + <> + + + + + ) + } +} + +export default SchemaItemCategories diff --git a/ui/src/flux/components/SchemaItemCategory.tsx b/ui/src/flux/components/SchemaItemCategory.tsx new file mode 100644 index 000000000..5b614da12 --- /dev/null +++ b/ui/src/flux/components/SchemaItemCategory.tsx @@ -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 { + 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 ( +
+
+
+
+ {this.categoryName} +
+
+ {!isUnopened && ( +
+ {this.itemList} +
+ )} +
+ ) + } + + 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 + + case CategoryType.Fields: + return + case CategoryType.Tags: + return + } + } + + 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 diff --git a/ui/src/flux/components/TagKeyList.tsx b/ui/src/flux/components/TagKeyList.tsx new file mode 100644 index 000000000..6eaecca35 --- /dev/null +++ b/ui/src/flux/components/TagKeyList.tsx @@ -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 { + 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 ( + <> +
+ +
+ {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 + } + + 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 => ( + + )) + } + return ( +
+
+
No more tag keys.
+
+
+ ) + } + + private async fetchTagKeys(): Promise { + 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) => { + this.setState({ + searchTerm: e.target.value, + }) + } + + private handleClick = (e: MouseEvent) => { + e.stopPropagation() + } +} + +export default TagKeyList diff --git a/ui/src/flux/components/TagKeyListItem.tsx b/ui/src/flux/components/TagKeyListItem.tsx new file mode 100644 index 000000000..e0bcfa451 --- /dev/null +++ b/ui/src/flux/components/TagKeyListItem.tsx @@ -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 { + 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 ( +
+
+
+
+ {tagKey} + Tag Key +
+ +
+ + Copy +
+
+
+ {!isUnopen && ( +
+ +
+ )} +
+ ) + } + + 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 diff --git a/ui/src/flux/components/TagList.tsx b/ui/src/flux/components/TagList.tsx deleted file mode 100644 index 295e0e458..000000000 --- a/ui/src/flux/components/TagList.tsx +++ /dev/null @@ -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 { - public render() { - const {db, source, tags, filter, notify} = this.props - - if (tags.length) { - return ( - <> - {tags.map(t => ( - - ))} - - ) - } - - return ( -
-
-
No more tag keys.
-
-
- ) - } - - private handleClick(e: MouseEvent) { - e.stopPropagation() - } -} diff --git a/ui/src/flux/components/TagListItem.tsx b/ui/src/flux/components/TagListItem.tsx deleted file mode 100644 index 900c6d0e7..000000000 --- a/ui/src/flux/components/TagListItem.tsx +++ /dev/null @@ -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 { - 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 ( -
-
-
-
- {tagKey} - Tag Key -
- -
- - Copy -
-
-
-
-
-
- - {this.isSearching && } -
- {this.count} -
- {this.isLoading && } - {!this.isLoading && ( - - )} -
-
- ) - } - - private get count(): JSX.Element { - const {count} = this.state - - if (!count) { - return - } - - let pluralizer = 's' - - if (count === 1) { - pluralizer = '' - } - - return ( -
{`${count} Tag Value${pluralizer}`}
- ) - } - - 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): void => { - const searchTerm = e.target.value - - this.setState({searchTerm, loadingSearch: RemoteDataState.Loading}, () => - this.debouncedOnSearch() - ) - } - - private handleInputClick = (e: MouseEvent): 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) => { - 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}` - } -} diff --git a/ui/src/flux/components/TagValueList.tsx b/ui/src/flux/components/TagValueList.tsx index f6b1f90d3..0583767ff 100644 --- a/ui/src/flux/components/TagValueList.tsx +++ b/ui/src/flux/components/TagValueList.tsx @@ -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 { - 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 { + 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) => ( - + - ))} - {shouldShowMoreValues && ( -
-
- -
-
- )} +
+ {this.tagValues} ) } - private handleClick = (e: MouseEvent) => { - 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 + if (loading === RemoteDataState.Loading) { + return } - 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 => ( + + )) + } + return ( +
+
+
No more tag values.
+
+
+ ) + } + + private async fetchTagValues(): Promise { + 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) => { + this.setState({ + searchTerm: e.target.value, + }) + } + + private handleClick = (e: MouseEvent) => { + e.stopPropagation() } } + +export default TagValueList diff --git a/ui/src/flux/components/TagValueListItem.tsx b/ui/src/flux/components/TagValueListItem.tsx index dc5b3900e..db2527c91 100644 --- a/ui/src/flux/components/TagValueListItem.tsx +++ b/ui/src/flux/components/TagValueListItem.tsx @@ -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 { - 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 ( -
+
- {value} + {tagValue} Tag Value
- +
Copy
-
- {this.isLoading && } - {!this.isLoading && ( - <> - {!!this.tags.length && ( -
- -
- )} - - - )} -
+ {!isUnopen && ( +
+ +
+ )}
) } - 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) => { - e.stopPropagation() - } - - private handleClick = (e: MouseEvent) => { + 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 { notify(notifyCopyToClipboardFailed(copiedText)) } } - - private onSearch = (e: ChangeEvent) => { - 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 diff --git a/ui/src/flux/constants/explorer.ts b/ui/src/flux/constants/explorer.ts index 5aed44f5f..ce2609797 100644 --- a/ui/src/flux/constants/explorer.ts +++ b/ui/src/flux/constants/explorer.ts @@ -1 +1,7 @@ export const TAG_VALUES_LIMIT = 10 + +export enum OpenState { + UNOPENED = 'unopened', + OPENED = 'opened', + ClOSED = 'closed', +} diff --git a/ui/src/shared/apis/flux/metaQueries.ts b/ui/src/shared/apis/flux/metaQueries.ts index 312d174f3..9fbb523f7 100644 --- a/ui/src/shared/apis/flux/metaQueries.ts +++ b/ui/src/shared/apis/flux/metaQueries.ts @@ -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 => { + return await tagValues({ + bucket, + source, + tagKey: '_field', + limit, + filter, + }) +} + export const tagKeys = async ( source: Source, bucket: string,