Enable pagination in tag value lists

Co-authored-by: Andrew Watkins <andrew.watkinz@gmail.com>
Co-authored-by: Chris Henn <chris.henn@influxdata.com>
pull/10616/head
Andrew Watkins 2018-05-30 16:01:20 -07:00
parent a86c2817a8
commit c254a8b5da
11 changed files with 264 additions and 91 deletions

View File

@ -8,19 +8,19 @@ const LoaderSkeleton: SFC = () => {
return (
<>
<div className="ifql-schema-tree ifql-tree-node" onClick={handleClick}>
<div className="ifql-schema-item skeleton">
<div className="ifql-schema-item no-hover">
<div className="ifql-schema-item-toggle" />
<div className="ifql-schema-item-skeleton" style={{width: '160px'}} />
</div>
</div>
<div className="ifql-schema-tree ifql-tree-node">
<div className="ifql-schema-item skeleton">
<div className="ifql-schema-item no-hover">
<div className="ifql-schema-item-toggle" />
<div className="ifql-schema-item-skeleton" style={{width: '200px'}} />
</div>
</div>
<div className="ifql-schema-tree ifql-tree-node">
<div className="ifql-schema-item skeleton">
<div className="ifql-schema-item no-hover">
<div className="ifql-schema-item-toggle" />
<div className="ifql-schema-item-skeleton" style={{width: '120px'}} />
</div>

View File

@ -0,0 +1,19 @@
import React, {SFC, CSSProperties} from 'react'
interface Props {
style?: CSSProperties
}
const LoadingSpinner: SFC<Props> = ({style}) => {
return (
<div className="loading-spinner" style={style}>
<div className="spinner">
<div className="bounce1" />
<div className="bounce2" />
<div className="bounce3" />
</div>
</div>
)
}
export default LoadingSpinner

View File

@ -1,15 +0,0 @@
import React, {SFC} from 'react'
const SearchSpinner: SFC = () => {
return (
<div className="search-spinner">
<div className="spinner">
<div className="bounce1" />
<div className="bounce2" />
<div className="bounce3" />
</div>
</div>
)
}
export default SearchSpinner

View File

@ -30,7 +30,7 @@ export default class TagList extends PureComponent<Props, State> {
<TagListItem
key={t}
db={db}
tag={t}
tagKey={t}
service={service}
filter={filter}
/>

View File

@ -1,16 +1,22 @@
import React, {PureComponent, ChangeEvent, MouseEvent} from 'react'
import React, {
PureComponent,
CSSProperties,
ChangeEvent,
MouseEvent,
} from 'react'
import _ from 'lodash'
import {Service, SchemaFilter, RemoteDataState} from 'src/types'
import {tagValues as fetchTagValues} from 'src/shared/apis/v2/metaQueries'
import {explorer} from 'src/ifql/constants'
import parseValuesColumn from 'src/shared/parsing/v2/tags'
import TagValueList from 'src/ifql/components/TagValueList'
import LoaderSkeleton from 'src/ifql/components/LoaderSkeleton'
import SearchSpinner from 'src/ifql/components/SearchSpinner'
import LoadingSpinner from 'src/ifql/components/LoadingSpinner'
interface Props {
tag: string
tagKey: string
db: string
service: Service
filter: SchemaFilter[]
@ -20,8 +26,11 @@ 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> {
@ -32,48 +41,68 @@ export default class TagListItem extends PureComponent<Props, State> {
isOpen: false,
loadingAll: RemoteDataState.NotStarted,
loadingSearch: RemoteDataState.NotStarted,
loadingMore: RemoteDataState.NotStarted,
tagValues: [],
count: null,
searchTerm: '',
limit: explorer.TAG_VALUES_LIMIT,
}
this.debouncedSearch = _.debounce(this.searchTagValues, 250)
this.debouncedOnSearch = _.debounce(() => {
this.searchTagValues()
this.getCount()
}, 250)
}
public async componentDidMount() {
this.getCount()
}
public render() {
const {tag, db, service, filter} = this.props
const {tagValues, searchTerm} = this.state
const {tagKey, db, service, filter} = this.props
const {tagValues, searchTerm, loadingMore, count, limit} = this.state
return (
<div className={this.className}>
<div className="ifql-schema-item" onClick={this.handleClick}>
<div className="ifql-schema-item-toggle" />
{tag}
{tagKey}
<span className="ifql-schema-type">Tag Key</span>
</div>
{this.state.isOpen && (
<>
<div className="ifql-schema--filter">
<input
className="form-control input-sm"
placeholder={`Filter within ${tag}`}
type="text"
spellCheck={false}
autoComplete="off"
value={searchTerm}
onClick={this.handleInputClick}
onChange={this.onSearch}
/>
{this.isSearching && <SearchSpinner />}
<div className="tag-value-list--header">
<div className="ifql-schema--filter">
<input
className="form-control input-sm"
placeholder={`Filter within ${tagKey}`}
type="text"
spellCheck={false}
autoComplete="off"
value={searchTerm}
onClick={this.handleInputClick}
onChange={this.onSearch}
/>
{this.isSearching && (
<LoadingSpinner style={this.spinnerStyle} />
)}
</div>
{!!count && `${count} total`}
</div>
{this.isLoading && <LoaderSkeleton />}
{!this.isLoading && (
<TagValueList
db={db}
service={service}
values={tagValues}
tag={tag}
filter={filter}
/>
<>
<TagValueList
db={db}
service={service}
values={tagValues}
tagKey={tagKey}
filter={filter}
onLoadMoreValues={this.handleLoadMoreValues}
isLoadingMoreValues={loadingMore === RemoteDataState.Loading}
shouldShowMoreValues={limit < count}
/>
</>
)}
</>
)}
@ -81,6 +110,14 @@ export default class TagListItem extends PureComponent<Props, State> {
)
}
private get spinnerStyle(): CSSProperties {
return {
position: 'absolute',
right: '15px',
top: '6px',
}
}
private get isSearching(): boolean {
return this.state.loadingSearch === RemoteDataState.Loading
}
@ -93,11 +130,11 @@ export default class TagListItem extends PureComponent<Props, State> {
const searchTerm = e.target.value
this.setState({searchTerm, loadingSearch: RemoteDataState.Loading}, () =>
this.debouncedSearch()
this.debouncedOnSearch()
)
}
private debouncedSearch() {} // See constructor
private debouncedOnSearch() {} // See constructor
private handleInputClick = (e: MouseEvent<HTMLInputElement>): void => {
e.stopPropagation()
@ -133,10 +170,33 @@ export default class TagListItem extends PureComponent<Props, State> {
}
}
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, service, tag, filter} = this.props
const {searchTerm} = this.state
const response = await fetchTagValues(service, db, filter, tag, searchTerm)
const {db, service, tagKey, filter} = this.props
const {searchTerm, limit} = this.state
const response = await fetchTagValues({
service,
db,
filter,
tagKey,
limit,
searchTerm,
})
return parseValuesColumn(response)
}
@ -151,6 +211,43 @@ export default class TagListItem extends PureComponent<Props, State> {
this.setState({isOpen: !this.state.isOpen})
}
private handleLoadMoreValues = (): void => {
const {limit} = this.state
this.setState(
{limit: limit + explorer.TAG_VALUES_LIMIT},
this.getMoreTagValues
)
}
private async getCount() {
const {service, db, filter, tagKey} = this.props
const {limit, searchTerm} = this.state
try {
const response = await fetchTagValues({
service,
db,
filter,
tagKey,
limit,
searchTerm,
count: true,
})
const parsed = parseValuesColumn(response)
if (parsed.length !== 1) {
throw new Error('Unexpected count response')
}
const count = Number(parsed[0])
this.setState({count})
} catch (error) {
console.error(error)
}
}
private get isFetchable(): boolean {
const {isOpen, loadingAll} = this.state

View File

@ -1,29 +1,72 @@
import React, {PureComponent} from 'react'
import React, {PureComponent, MouseEvent} from 'react'
import TagValueListItem from 'src/ifql/components/TagValueListItem'
import LoadingSpinner from 'src/ifql/components/LoadingSpinner'
import {Service, SchemaFilter} from 'src/types'
import {explorer} from 'src/ifql/constants'
interface Props {
service: Service
db: string
tag: string
tagKey: string
values: string[]
filter: SchemaFilter[]
isLoadingMoreValues: boolean
onLoadMoreValues: () => void
shouldShowMoreValues: boolean
}
export default class TagValueList extends PureComponent<Props> {
public render() {
const {db, service, values, tag, filter} = this.props
const {
db,
service,
values,
tagKey,
filter,
shouldShowMoreValues,
} = this.props
return values.map((v, i) => (
<TagValueListItem
key={i}
db={db}
tag={tag}
value={v}
service={service}
filter={filter}
/>
))
return (
<>
{values.map((v, i) => (
<TagValueListItem
key={i}
db={db}
value={v}
tagKey={tagKey}
service={service}
filter={filter}
/>
))}
{shouldShowMoreValues && (
<div className="ifql-schema-tree ifql-tree-node">
<div className="ifql-schema-item no-hover">
<button
className="btn btn-xs btn-default increase-values-limit"
onClick={this.handleClick}
>
{this.buttonValue}
</button>
</div>
</div>
)}
</>
)
}
private handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
this.props.onLoadMoreValues()
}
private get buttonValue(): string | JSX.Element {
const {isLoadingMoreValues} = this.props
if (isLoadingMoreValues) {
return <LoadingSpinner />
}
return `Load next ${explorer.TAG_VALUES_LIMIT} values`
}
}

View File

@ -9,7 +9,7 @@ import {Service, SchemaFilter, RemoteDataState} from 'src/types'
interface Props {
db: string
service: Service
tag: string
tagKey: string
value: string
filter: SchemaFilter[]
}
@ -79,9 +79,9 @@ class TagValueListItem extends PureComponent<Props, State> {
}
private get filter(): SchemaFilter[] {
const {filter, tag, value} = this.props
const {filter, tagKey, value} = this.props
return [...filter, {key: tag, value}]
return [...filter, {key: tagKey, value}]
}
private get tags(): string[] {

View File

@ -0,0 +1 @@
export const TAG_VALUES_LIMIT = 2

View File

@ -4,5 +4,6 @@ import * as argTypes from 'src/ifql/constants/argumentTypes'
import * as funcNames from 'src/ifql/constants/funcNames'
import * as builder from 'src/ifql/constants/builder'
import * as vis from 'src/ifql/constants/vis'
import * as explorer from 'src/ifql/constants/explorer'
export {ast, funcNames, argTypes, editor, builder, vis}
export {ast, funcNames, argTypes, editor, builder, vis, explorer}

View File

@ -44,19 +44,34 @@ export const tagKeys = async (
return proxy(service, script)
}
export const tagValues = async (
service: Service,
db: string,
filter: SchemaFilter[],
tagKey: string,
searchTerm: string = ''
): Promise<any> => {
interface TagValuesParams {
service: Service
db: string
tagKey: string
limit: number
filter?: SchemaFilter[]
searchTerm?: string
count?: boolean
}
export const tagValues = async ({
db,
service,
tagKey,
limit,
filter = [],
searchTerm = '',
count = false,
}: TagValuesParams): Promise<any> => {
let regexFilter = ''
if (searchTerm) {
regexFilter = `|> filter(fn: (r) => r.${tagKey} =~ /${searchTerm}/)`
}
const limitFunc = count ? '' : `|> limit(n:${limit})`
const countFunc = count ? '|> count()' : ''
const script = `
from(db:"${db}")
|> range(start:-1h)
@ -64,8 +79,9 @@ export const tagValues = async (
${tagsetFilter(filter)}
|> group(by:["${tagKey}"])
|> distinct(column:"${tagKey}")
|> group(none: true)
|> limit(n:100)
|> group(by:["_stop","_start"])
${limitFunc}
${countFunc}
`
return proxy(service, script)

View File

@ -74,7 +74,7 @@ $ifql-tree-line: 2px;
left: $ifql-tree-indent / 2;
transform: translate(-50%, -50%);
}
&:not(.skeleton):hover {
&:not(.no-hover):hover {
color: $g17-whisper;
cursor: pointer;
background-color: $g4-onyx;
@ -111,6 +111,10 @@ $ifql-tree-line: 2px;
color: $g11-sidewalk;
cursor: default;
}
.increase-values-limit {
margin-left: 8px;
width: 160px;
}
}
@keyframes skeleton-animation {
@ -194,10 +198,22 @@ $ifql-tree-line: 2px;
margin-right: $ifql-tree-indent / 2;
margin-top: 1px;
margin-bottom: 5px;
input {
height: 25px;
font-size: 12px;
}
max-width: 220px;
}
.ifql-schema--filter>input.input-sm {
height: 25px;
font-size: 12px;
}
.tag-value-list--header>.ifql-schema--filter {
display: inline-block;
margin-right: 15px;
}
.tag-value-list--header {
font-size: 12px;
color: $g11-sidewalk;
}
/*
@ -207,18 +223,13 @@ $ifql-tree-line: 2px;
From http: //tobiasahlin.com/spinkit/.
*/
.search-spinner {
position: absolute;
right: 15px;
top: 2px;
}
.search-spinner .spinner {
.loading-spinner .spinner {
width: 25px;
text-align: center;
margin: 0 auto;
}
.search-spinner .spinner>div {
.loading-spinner .spinner>div {
width: 8px;
height: 8px;
background-color: $g8-storm;
@ -227,11 +238,11 @@ $ifql-tree-line: 2px;
animation: sk-bouncedelay 1s infinite ease-in-out both;
}
.search-spinner .spinner .bounce1 {
.loading-spinner .spinner .bounce1 {
animation-delay: -0.32s;
}
.search-spinner .spinner .bounce2 {
.loading-spinner .spinner .bounce2 {
animation-delay: -0.16s;
}