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
parent
a86c2817a8
commit
c254a8b5da
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[] {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const TAG_VALUES_LIMIT = 2
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue