test(ui): component integration testing w/ mock redux store (#11577)
* wip: introduce connected async component test * test(query-builder): mock findBuckets * test(ui): add ability to pass arbitrary state to renderWithRedux * test(ui/queryBuilder): click bucket and tag * test(ui/queryBuilder): use regex matchers to query all * test(ui/queryBuilder): it can click a tag value * chore: change id to ID * chore: move QueryBuilderFetcher * test(ui/queryBuilder): try waiting for dropdown-items * test(ui/queryBuilder): move initial state out of beforeEach * test(ui/queryBuilder): get by bucket name * wip * test: put back to default * test: remove awaitpull/11623/head
parent
c72363c7ec
commit
854b2a43eb
|
@ -0,0 +1,6 @@
|
||||||
|
import {cleanup} from 'react-testing-library'
|
||||||
|
|
||||||
|
// cleans up state between react-testing-library tests
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
|
@ -1,11 +1,5 @@
|
||||||
import {
|
import {Template, SourceLinks, TemplateType, TemplateValueType} from 'src/types'
|
||||||
Source,
|
import {Source} from 'src/api'
|
||||||
SourceAuthenticationMethod,
|
|
||||||
Template,
|
|
||||||
SourceLinks,
|
|
||||||
TemplateType,
|
|
||||||
TemplateValueType,
|
|
||||||
} from 'src/types'
|
|
||||||
import {Cell, Dashboard, Label} from 'src/types/v2'
|
import {Cell, Dashboard, Label} from 'src/types/v2'
|
||||||
import {Links} from 'src/types/v2/links'
|
import {Links} from 'src/types/v2/links'
|
||||||
import {Task} from 'src/types/v2/tasks'
|
import {Task} from 'src/types/v2/tasks'
|
||||||
|
@ -123,14 +117,12 @@ export const sourceLinks: SourceLinks = {
|
||||||
export const source: Source = {
|
export const source: Source = {
|
||||||
id: '16',
|
id: '16',
|
||||||
name: 'ssl',
|
name: 'ssl',
|
||||||
type: 'influx',
|
type: Source.TypeEnum.Self,
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
url: 'https://localhost:9086',
|
url: 'https://localhost:9086',
|
||||||
insecureSkipVerify: true,
|
insecureSkipVerify: true,
|
||||||
default: false,
|
|
||||||
telegraf: 'telegraf',
|
telegraf: 'telegraf',
|
||||||
links: sourceLinks,
|
links: sourceLinks,
|
||||||
authentication: SourceAuthenticationMethod.Basic,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const timeRange = {
|
export const timeRange = {
|
||||||
|
@ -583,7 +575,11 @@ export const setSetupParamsResponse = {
|
||||||
userID: '033bc62520fe3000',
|
userID: '033bc62520fe3000',
|
||||||
user: 'iris',
|
user: 'iris',
|
||||||
permissions: [
|
permissions: [
|
||||||
{action: 'read', resource: 'authorizations', orgID: '033bc62534be3000'},
|
{
|
||||||
|
action: 'read',
|
||||||
|
resource: 'authorizations',
|
||||||
|
orgID: '033bc62534be3000',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
action: 'write',
|
action: 'write',
|
||||||
resource: 'authorizations',
|
resource: 'authorizations',
|
||||||
|
|
|
@ -902,6 +902,12 @@
|
||||||
"physical-cpu-count": "^2.0.0"
|
"physical-cpu-count": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@sheerun/mutationobserver-shim": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/abstract-leveldown": {
|
"@types/abstract-leveldown": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-5.0.1.tgz",
|
||||||
|
@ -3738,6 +3744,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dom-testing-library": {
|
||||||
|
"version": "3.16.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-testing-library/-/dom-testing-library-3.16.4.tgz",
|
||||||
|
"integrity": "sha512-D8tFLGe0xInL8F/KxZM7gc4r/vOCTgFGM93zXLB/AjFPz2O86y0UaruXl45K6xhqyclJFHHxUtgwaRddRyqxFw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.1.5",
|
||||||
|
"@sheerun/mutationobserver-shim": "^0.3.2",
|
||||||
|
"pretty-format": "^23.6.0",
|
||||||
|
"wait-for-expect": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain-browser": {
|
"domain-browser": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
|
||||||
|
@ -10629,6 +10647,15 @@
|
||||||
"schedule": "^0.5.0"
|
"schedule": "^0.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-testing-library": {
|
||||||
|
"version": "5.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-testing-library/-/react-testing-library-5.4.4.tgz",
|
||||||
|
"integrity": "sha512-/TiERZ+URSNhZQfjrUXh0VLsiLSmhqP1WP+2e2wWqWqrRIWpcAxrfuBxzlT75LYMDNmicEikaXJqRDi/pqCEDg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"dom-testing-library": "^3.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-tooltip": {
|
"react-tooltip": {
|
||||||
"version": "3.8.4",
|
"version": "3.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-3.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-3.8.4.tgz",
|
||||||
|
@ -13000,6 +13027,12 @@
|
||||||
"browser-process-hrtime": "^0.1.2"
|
"browser-process-hrtime": "^0.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"wait-for-expect": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-vQDokqxyMyknfX3luCDn16bSaRcOyH6gGuUXMIbxBLeTo6nWuEWYqMTT9a+44FmW8c2m6TRWBdNvBBjA1hwEKg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"walker": {
|
"walker": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz",
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"tsc:watch": "tsc -p ./tsconfig.json --noEmit --pretty -w"
|
"tsc:watch": "tsc -p ./tsconfig.json --noEmit --pretty -w"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
"setupTestFrameworkScriptFile": "./jestSetup.ts",
|
||||||
"displayName": "test",
|
"displayName": "test",
|
||||||
"testURL": "http://localhost",
|
"testURL": "http://localhost",
|
||||||
"testPathIgnorePatterns": [
|
"testPathIgnorePatterns": [
|
||||||
|
@ -104,6 +105,7 @@
|
||||||
"jsdom": "^9.0.0",
|
"jsdom": "^9.0.0",
|
||||||
"parcel": "^1.11.0",
|
"parcel": "^1.11.0",
|
||||||
"prettier": "^1.14.3",
|
"prettier": "^1.14.3",
|
||||||
|
"react-testing-library": "^5.4.4",
|
||||||
"sass": "^1.15.3",
|
"sass": "^1.15.3",
|
||||||
"ts-jest": "^23.10.3",
|
"ts-jest": "^23.10.3",
|
||||||
"tslib": "^1.9.0",
|
"tslib": "^1.9.0",
|
||||||
|
|
|
@ -43,6 +43,8 @@ export interface Props {
|
||||||
mode?: DropdownMode
|
mode?: DropdownMode
|
||||||
titleText?: string
|
titleText?: string
|
||||||
menuHeader?: JSX.Element
|
menuHeader?: JSX.Element
|
||||||
|
testID: string
|
||||||
|
buttonTestID: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -122,6 +124,7 @@ class Dropdown extends Component<Props, State> {
|
||||||
icon,
|
icon,
|
||||||
children,
|
children,
|
||||||
titleText,
|
titleText,
|
||||||
|
buttonTestID,
|
||||||
} = this.props
|
} = this.props
|
||||||
const {expanded} = this.state
|
const {expanded} = this.state
|
||||||
|
|
||||||
|
@ -147,6 +150,7 @@ class Dropdown extends Component<Props, State> {
|
||||||
onClick={this.toggleMenu}
|
onClick={this.toggleMenu}
|
||||||
status={status}
|
status={status}
|
||||||
title={titleText}
|
title={titleText}
|
||||||
|
testID={buttonTestID}
|
||||||
>
|
>
|
||||||
{dropdownLabel}
|
{dropdownLabel}
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
|
@ -160,6 +164,7 @@ class Dropdown extends Component<Props, State> {
|
||||||
menuHeader,
|
menuHeader,
|
||||||
menuColor,
|
menuColor,
|
||||||
children,
|
children,
|
||||||
|
testID,
|
||||||
} = this.props
|
} = this.props
|
||||||
const {expanded} = this.state
|
const {expanded} = this.state
|
||||||
|
|
||||||
|
@ -174,7 +179,10 @@ class Dropdown extends Component<Props, State> {
|
||||||
autoHeight={true}
|
autoHeight={true}
|
||||||
maxHeight={maxMenuHeight}
|
maxHeight={maxMenuHeight}
|
||||||
>
|
>
|
||||||
<div className="dropdown--menu">
|
<div
|
||||||
|
className="dropdown--menu"
|
||||||
|
data-testid={`dropdown--menu ${testID}`}
|
||||||
|
>
|
||||||
{menuHeader && menuHeader}
|
{menuHeader && menuHeader}
|
||||||
{React.Children.map(children, (child: JSX.Element) => {
|
{React.Children.map(children, (child: JSX.Element) => {
|
||||||
if (this.childTypeIsValid(child)) {
|
if (this.childTypeIsValid(child)) {
|
||||||
|
|
|
@ -23,6 +23,7 @@ interface Props {
|
||||||
size?: ComponentSize
|
size?: ComponentSize
|
||||||
icon?: IconFont
|
icon?: IconFont
|
||||||
title?: string
|
title?: string
|
||||||
|
testID: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ErrorHandling
|
@ErrorHandling
|
||||||
|
@ -35,7 +36,7 @@ class DropdownButton extends Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {onClick, children, title} = this.props
|
const {onClick, children, title, testID} = this.props
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={this.classname}
|
className={this.classname}
|
||||||
|
@ -43,6 +44,7 @@ class DropdownButton extends Component<Props> {
|
||||||
disabled={this.isDisabled}
|
disabled={this.isDisabled}
|
||||||
title={title}
|
title={title}
|
||||||
type={ButtonType.Button}
|
type={ButtonType.Button}
|
||||||
|
data-testid={testID}
|
||||||
>
|
>
|
||||||
{this.icon}
|
{this.icon}
|
||||||
<span className="dropdown--selected">{children}</span>
|
<span className="dropdown--selected">{children}</span>
|
||||||
|
|
|
@ -14,6 +14,7 @@ interface Props {
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
checkbox?: boolean
|
checkbox?: boolean
|
||||||
onClick?: (value: any) => void
|
onClick?: (value: any) => void
|
||||||
|
testid?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ErrorHandling
|
@ErrorHandling
|
||||||
|
@ -24,7 +25,7 @@ class DropdownItem extends Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const {selected, checkbox} = this.props
|
const {selected, checkbox, id} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -33,6 +34,7 @@ class DropdownItem extends Component<Props> {
|
||||||
active: selected && !checkbox,
|
active: selected && !checkbox,
|
||||||
'multi-select--item': checkbox,
|
'multi-select--item': checkbox,
|
||||||
})}
|
})}
|
||||||
|
data-testid={`dropdown--item ${id}`}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
>
|
>
|
||||||
{this.checkBox}
|
{this.checkBox}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {setActiveTimeMachine} from 'src/timeMachine/actions'
|
||||||
// Utils
|
// Utils
|
||||||
import {DE_TIME_MACHINE_ID} from 'src/timeMachine/constants'
|
import {DE_TIME_MACHINE_ID} from 'src/timeMachine/constants'
|
||||||
import {HoverTimeProvider} from 'src/dashboards/utils/hoverTime'
|
import {HoverTimeProvider} from 'src/dashboards/utils/hoverTime'
|
||||||
import {queryBuilderFetcher} from 'src/timeMachine/apis/queryBuilder'
|
import {queryBuilderFetcher} from 'src/timeMachine/apis/QueryBuilderFetcher'
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
import './DataExplorer.scss'
|
import './DataExplorer.scss'
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {Provider} from 'react-redux'
|
||||||
|
|
||||||
|
import {render} from 'react-testing-library'
|
||||||
|
import configureStore from 'src/store/configureStore'
|
||||||
|
import {createMemoryHistory} from 'history'
|
||||||
|
|
||||||
|
const localState = {
|
||||||
|
app: {
|
||||||
|
ephemeral: {
|
||||||
|
inPresentationMode: false,
|
||||||
|
},
|
||||||
|
persisted: {autoRefresh: 0, showTemplateControlBar: false},
|
||||||
|
},
|
||||||
|
VERSION: '2.0.0',
|
||||||
|
ranges: [
|
||||||
|
{
|
||||||
|
dashboardID: '0349ecda531ea000',
|
||||||
|
seconds: 900,
|
||||||
|
lower: 'now() - 15m',
|
||||||
|
upper: null,
|
||||||
|
label: 'Past 15m',
|
||||||
|
duration: '15m',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = createMemoryHistory({entries: ['/']})
|
||||||
|
|
||||||
|
export function renderWithRedux(ui, initialState = s => s) {
|
||||||
|
const seedStore = configureStore(localState, history)
|
||||||
|
const seedState = seedStore.getState()
|
||||||
|
const store = configureStore(initialState(seedState), history)
|
||||||
|
|
||||||
|
const provider = <Provider store={store}>{ui}</Provider>
|
||||||
|
|
||||||
|
return {
|
||||||
|
...render(provider),
|
||||||
|
store,
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ export default class SearchableDropdown extends Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {searchTerm, searchPlaceholder, buttonSize} = this.props
|
const {searchTerm, searchPlaceholder, buttonSize, testID} = this.props
|
||||||
|
|
||||||
const dropdownProps = omit(this.props, [
|
const dropdownProps = omit(this.props, [
|
||||||
'searchTerm',
|
'searchTerm',
|
||||||
|
@ -50,6 +50,7 @@ export default class SearchableDropdown extends Component<Props> {
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
testID={testID}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,12 +28,9 @@ import protosReducer from 'src/protos/reducers'
|
||||||
import {LocalStorage} from 'src/types/localStorage'
|
import {LocalStorage} from 'src/types/localStorage'
|
||||||
import {AppState} from 'src/types/v2'
|
import {AppState} from 'src/types/v2'
|
||||||
|
|
||||||
type ReducerState = Pick<
|
type ReducerState = Pick<AppState, Exclude<keyof AppState, 'timeRange'>>
|
||||||
AppState,
|
|
||||||
Exclude<keyof AppState, 'VERSION' | 'timeRange'>
|
|
||||||
>
|
|
||||||
|
|
||||||
const rootReducer = combineReducers<ReducerState>({
|
export const rootReducer = combineReducers<ReducerState>({
|
||||||
...sharedReducers,
|
...sharedReducers,
|
||||||
ranges: rangesReducer,
|
ranges: rangesReducer,
|
||||||
dashboards: dashboardsReducer,
|
dashboards: dashboardsReducer,
|
||||||
|
@ -49,6 +46,7 @@ const rootReducer = combineReducers<ReducerState>({
|
||||||
noteEditor: noteEditorReducer,
|
noteEditor: noteEditorReducer,
|
||||||
dataLoading: dataLoadingReducer,
|
dataLoading: dataLoadingReducer,
|
||||||
protos: protosReducer,
|
protos: protosReducer,
|
||||||
|
VERSION: () => '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const composeEnhancers =
|
const composeEnhancers =
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// APIs
|
// APIs
|
||||||
import {queryBuilderFetcher} from 'src/timeMachine/apis/queryBuilder'
|
import {queryBuilderFetcher} from 'src/timeMachine/apis/QueryBuilderFetcher'
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import {
|
import {
|
||||||
|
@ -47,7 +47,9 @@ interface SetBuilderBucketsAction {
|
||||||
payload: {buckets: string[]}
|
payload: {buckets: string[]}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setBuilderBuckets = (buckets: string[]): SetBuilderBucketsAction => ({
|
export const setBuilderBuckets = (
|
||||||
|
buckets: string[]
|
||||||
|
): SetBuilderBucketsAction => ({
|
||||||
type: 'SET_BUILDER_BUCKETS',
|
type: 'SET_BUILDER_BUCKETS',
|
||||||
payload: {buckets},
|
payload: {buckets},
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
// APIs
|
||||||
|
import {
|
||||||
|
findBuckets,
|
||||||
|
findKeys,
|
||||||
|
findValues,
|
||||||
|
} from 'src/timeMachine/apis/queryBuilder'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import {BuilderConfig} from 'src/types/v2'
|
||||||
|
import {WrappedCancelablePromise} from 'src/types/promises'
|
||||||
|
|
||||||
|
type CancelableQuery = WrappedCancelablePromise<string[]>
|
||||||
|
|
||||||
|
class QueryBuilderFetcher {
|
||||||
|
private findBucketsQuery: CancelableQuery
|
||||||
|
private findKeysQueries: CancelableQuery[] = []
|
||||||
|
private findValuesQueries: CancelableQuery[] = []
|
||||||
|
private findKeysCache: {[key: string]: string[]} = {}
|
||||||
|
private findValuesCache: {[key: string]: string[]} = {}
|
||||||
|
private findBucketsCache: {[key: string]: string[]} = {}
|
||||||
|
|
||||||
|
public async findBuckets(url: string): Promise<string[]> {
|
||||||
|
this.cancelFindBuckets()
|
||||||
|
|
||||||
|
const cacheKey = JSON.stringify([...arguments])
|
||||||
|
const cachedResult = this.findBucketsCache[cacheKey]
|
||||||
|
|
||||||
|
if (cachedResult) {
|
||||||
|
return Promise.resolve(cachedResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingResult = findBuckets(url)
|
||||||
|
|
||||||
|
pendingResult.promise.then(result => {
|
||||||
|
this.findBucketsCache[cacheKey] = result
|
||||||
|
})
|
||||||
|
|
||||||
|
return pendingResult.promise
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelFindBuckets(): void {
|
||||||
|
if (this.findBucketsQuery) {
|
||||||
|
this.findBucketsQuery.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findKeys(
|
||||||
|
index: number,
|
||||||
|
url: string,
|
||||||
|
bucket: string,
|
||||||
|
tagsSelections: BuilderConfig['tags'],
|
||||||
|
searchTerm: string = ''
|
||||||
|
): Promise<string[]> {
|
||||||
|
this.cancelFindKeys(index)
|
||||||
|
|
||||||
|
const cacheKey = JSON.stringify([...arguments].slice(1))
|
||||||
|
const cachedResult = this.findKeysCache[cacheKey]
|
||||||
|
|
||||||
|
if (cachedResult) {
|
||||||
|
return Promise.resolve(cachedResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingResult = findKeys(url, bucket, tagsSelections, searchTerm)
|
||||||
|
|
||||||
|
this.findKeysQueries[index] = pendingResult
|
||||||
|
|
||||||
|
pendingResult.promise.then(result => {
|
||||||
|
this.findKeysCache[cacheKey] = result
|
||||||
|
})
|
||||||
|
|
||||||
|
return pendingResult.promise
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelFindKeys(index: number): void {
|
||||||
|
if (this.findKeysQueries[index]) {
|
||||||
|
this.findKeysQueries[index].cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findValues(
|
||||||
|
index: number,
|
||||||
|
url: string,
|
||||||
|
bucket: string,
|
||||||
|
tagsSelections: BuilderConfig['tags'],
|
||||||
|
key: string,
|
||||||
|
searchTerm: string = ''
|
||||||
|
): Promise<string[]> {
|
||||||
|
this.cancelFindValues(index)
|
||||||
|
|
||||||
|
const cacheKey = JSON.stringify([...arguments].slice(1))
|
||||||
|
const cachedResult = this.findValuesCache[cacheKey]
|
||||||
|
|
||||||
|
if (cachedResult) {
|
||||||
|
return Promise.resolve(cachedResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingResult = findValues(
|
||||||
|
url,
|
||||||
|
bucket,
|
||||||
|
tagsSelections,
|
||||||
|
key,
|
||||||
|
searchTerm
|
||||||
|
)
|
||||||
|
|
||||||
|
this.findValuesQueries[index] = pendingResult
|
||||||
|
|
||||||
|
pendingResult.promise.then(result => {
|
||||||
|
this.findValuesCache[cacheKey] = result
|
||||||
|
})
|
||||||
|
|
||||||
|
return pendingResult.promise
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelFindValues(index: number): void {
|
||||||
|
if (this.findValuesQueries[index]) {
|
||||||
|
this.findValuesQueries[index].cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearCache(): void {
|
||||||
|
this.findBucketsCache = {}
|
||||||
|
this.findKeysCache = {}
|
||||||
|
this.findValuesCache = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const queryBuilderFetcher = new QueryBuilderFetcher()
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const findBuckets = (_: string) => ({
|
||||||
|
promise: Promise.resolve(['b1', 'b2']),
|
||||||
|
cancel: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const findKeys = (_: string) => ({
|
||||||
|
promise: Promise.resolve(['tk1', 'tk2']),
|
||||||
|
cancel: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const findValues = (_: string) => ({
|
||||||
|
promise: Promise.resolve(['tv1', 'tv2']),
|
||||||
|
cancel: () => {},
|
||||||
|
})
|
|
@ -14,7 +14,7 @@ export const LIMIT = 200
|
||||||
|
|
||||||
type CancelableQuery = WrappedCancelablePromise<string[]>
|
type CancelableQuery = WrappedCancelablePromise<string[]>
|
||||||
|
|
||||||
function findBuckets(url: string): CancelableQuery {
|
export function findBuckets(url: string): CancelableQuery {
|
||||||
const query = `buckets()
|
const query = `buckets()
|
||||||
|> sort(columns: ["name"])
|
|> sort(columns: ["name"])
|
||||||
|> limit(n: ${LIMIT})`
|
|> limit(n: ${LIMIT})`
|
||||||
|
@ -27,7 +27,7 @@ function findBuckets(url: string): CancelableQuery {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findKeys(
|
export function findKeys(
|
||||||
url: string,
|
url: string,
|
||||||
bucket: string,
|
bucket: string,
|
||||||
tagsSelections: BuilderConfig['tags'],
|
tagsSelections: BuilderConfig['tags'],
|
||||||
|
@ -57,7 +57,7 @@ v1.tagKeys(bucket: "${bucket}", predicate: ${tagFilters}, start: -${SEARCH_DURAT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findValues(
|
export function findValues(
|
||||||
url: string,
|
url: string,
|
||||||
bucket: string,
|
bucket: string,
|
||||||
tagsSelections: BuilderConfig['tags'],
|
tagsSelections: BuilderConfig['tags'],
|
||||||
|
@ -81,7 +81,10 @@ v1.tagValues(bucket: "${bucket}", tag: "${key}", predicate: ${tagFilters}, start
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractCol(resp: ExecuteFluxQueryResult, colName: string): string[] {
|
export function extractCol(
|
||||||
|
resp: ExecuteFluxQueryResult,
|
||||||
|
colName: string
|
||||||
|
): string[] {
|
||||||
const tables = parseResponse(resp.csv)
|
const tables = parseResponse(resp.csv)
|
||||||
const data = get(tables, '0.data', [])
|
const data = get(tables, '0.data', [])
|
||||||
|
|
||||||
|
@ -104,7 +107,9 @@ function extractCol(resp: ExecuteFluxQueryResult, colName: string): string[] {
|
||||||
return colValues
|
return colValues
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTagFilterPredicate(tagsSelections: BuilderConfig['tags']) {
|
export function formatTagFilterPredicate(
|
||||||
|
tagsSelections: BuilderConfig['tags']
|
||||||
|
) {
|
||||||
const validSelections = tagsSelections.filter(
|
const validSelections = tagsSelections.filter(
|
||||||
({key, values}) => key && values.length
|
({key, values}) => key && values.length
|
||||||
)
|
)
|
||||||
|
@ -124,7 +129,7 @@ function formatTagFilterPredicate(tagsSelections: BuilderConfig['tags']) {
|
||||||
return `(r) => ${calls}`
|
return `(r) => ${calls}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTagKeyFilterCall(tagsSelections: BuilderConfig['tags']) {
|
export function formatTagKeyFilterCall(tagsSelections: BuilderConfig['tags']) {
|
||||||
const keys = tagsSelections.map(({key}) => key)
|
const keys = tagsSelections.map(({key}) => key)
|
||||||
|
|
||||||
if (!keys.length) {
|
if (!keys.length) {
|
||||||
|
@ -136,125 +141,10 @@ function formatTagKeyFilterCall(tagsSelections: BuilderConfig['tags']) {
|
||||||
return `\n |> filter(fn: (r) => ${fnBody})`
|
return `\n |> filter(fn: (r) => ${fnBody})`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSearchFilterCall(searchTerm: string) {
|
export function formatSearchFilterCall(searchTerm: string) {
|
||||||
if (!searchTerm) {
|
if (!searchTerm) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
return `\n |> filter(fn: (r) => r._value =~ /(?i:${searchTerm})/)`
|
return `\n |> filter(fn: (r) => r._value =~ /(?i:${searchTerm})/)`
|
||||||
}
|
}
|
||||||
|
|
||||||
class QueryBuilderFetcher {
|
|
||||||
private findBucketsQuery: CancelableQuery
|
|
||||||
private findKeysQueries: CancelableQuery[] = []
|
|
||||||
private findValuesQueries: CancelableQuery[] = []
|
|
||||||
private findKeysCache: {[key: string]: string[]} = {}
|
|
||||||
private findValuesCache: {[key: string]: string[]} = {}
|
|
||||||
private findBucketsCache: {[key: string]: string[]} = {}
|
|
||||||
|
|
||||||
public async findBuckets(url: string): Promise<string[]> {
|
|
||||||
this.cancelFindBuckets()
|
|
||||||
|
|
||||||
const cacheKey = JSON.stringify([...arguments])
|
|
||||||
const cachedResult = this.findBucketsCache[cacheKey]
|
|
||||||
|
|
||||||
if (cachedResult) {
|
|
||||||
return Promise.resolve(cachedResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingResult = findBuckets(url)
|
|
||||||
|
|
||||||
pendingResult.promise.then(result => {
|
|
||||||
this.findBucketsCache[cacheKey] = result
|
|
||||||
})
|
|
||||||
|
|
||||||
return pendingResult.promise
|
|
||||||
}
|
|
||||||
|
|
||||||
public cancelFindBuckets(): void {
|
|
||||||
if (this.findBucketsQuery) {
|
|
||||||
this.findBucketsQuery.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async findKeys(
|
|
||||||
index: number,
|
|
||||||
url: string,
|
|
||||||
bucket: string,
|
|
||||||
tagsSelections: BuilderConfig['tags'],
|
|
||||||
searchTerm: string = ''
|
|
||||||
): Promise<string[]> {
|
|
||||||
this.cancelFindKeys(index)
|
|
||||||
|
|
||||||
const cacheKey = JSON.stringify([...arguments].slice(1))
|
|
||||||
const cachedResult = this.findKeysCache[cacheKey]
|
|
||||||
|
|
||||||
if (cachedResult) {
|
|
||||||
return Promise.resolve(cachedResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingResult = findKeys(url, bucket, tagsSelections, searchTerm)
|
|
||||||
|
|
||||||
this.findKeysQueries[index] = pendingResult
|
|
||||||
|
|
||||||
pendingResult.promise.then(result => {
|
|
||||||
this.findKeysCache[cacheKey] = result
|
|
||||||
})
|
|
||||||
|
|
||||||
return pendingResult.promise
|
|
||||||
}
|
|
||||||
|
|
||||||
public cancelFindKeys(index: number): void {
|
|
||||||
if (this.findKeysQueries[index]) {
|
|
||||||
this.findKeysQueries[index].cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async findValues(
|
|
||||||
index: number,
|
|
||||||
url: string,
|
|
||||||
bucket: string,
|
|
||||||
tagsSelections: BuilderConfig['tags'],
|
|
||||||
key: string,
|
|
||||||
searchTerm: string = ''
|
|
||||||
): Promise<string[]> {
|
|
||||||
this.cancelFindValues(index)
|
|
||||||
|
|
||||||
const cacheKey = JSON.stringify([...arguments].slice(1))
|
|
||||||
const cachedResult = this.findValuesCache[cacheKey]
|
|
||||||
|
|
||||||
if (cachedResult) {
|
|
||||||
return Promise.resolve(cachedResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingResult = findValues(
|
|
||||||
url,
|
|
||||||
bucket,
|
|
||||||
tagsSelections,
|
|
||||||
key,
|
|
||||||
searchTerm
|
|
||||||
)
|
|
||||||
|
|
||||||
this.findValuesQueries[index] = pendingResult
|
|
||||||
|
|
||||||
pendingResult.promise.then(result => {
|
|
||||||
this.findValuesCache[cacheKey] = result
|
|
||||||
})
|
|
||||||
|
|
||||||
return pendingResult.promise
|
|
||||||
}
|
|
||||||
|
|
||||||
public cancelFindValues(index: number): void {
|
|
||||||
if (this.findValuesQueries[index]) {
|
|
||||||
this.findValuesQueries[index].cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearCache(): void {
|
|
||||||
this.findBucketsCache = {}
|
|
||||||
this.findKeysCache = {}
|
|
||||||
this.findValuesCache = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const queryBuilderFetcher = new QueryBuilderFetcher()
|
|
||||||
|
|
|
@ -143,6 +143,7 @@ const mstp = (state: AppState) => {
|
||||||
const {draftQueries, timeRange, isViewingRawData} = getActiveTimeMachine(
|
const {draftQueries, timeRange, isViewingRawData} = getActiveTimeMachine(
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
|
|
||||||
const activeQuery = getActiveQuery(state)
|
const activeQuery = getActiveQuery(state)
|
||||||
|
|
||||||
return {timeRange, activeQuery, draftQueries, isViewingRawData}
|
return {timeRange, activeQuery, draftQueries, isViewingRawData}
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import {renderWithRedux} from 'src/mockState'
|
||||||
|
import {source} from 'mocks/dummyData'
|
||||||
|
import {waitForElement, fireEvent} from 'react-testing-library'
|
||||||
|
|
||||||
|
import QueryBuilder from 'src/timeMachine/components/QueryBuilder'
|
||||||
|
|
||||||
|
jest.mock('src/timeMachine/apis/queryBuilder')
|
||||||
|
|
||||||
|
const setInitialState = state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
sources: {
|
||||||
|
activeSourceID: source.id,
|
||||||
|
sources: {
|
||||||
|
[source.id]: source,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('QueryBuilder', () => {
|
||||||
|
it('can select a bucket', async () => {
|
||||||
|
const {
|
||||||
|
getByTestId,
|
||||||
|
getAllByTestId,
|
||||||
|
queryAllByTestId,
|
||||||
|
getByText,
|
||||||
|
} = renderWithRedux(<QueryBuilder />, setInitialState)
|
||||||
|
|
||||||
|
const bucketsDropdownClosed = await waitForElement(() => getByText('b1'))
|
||||||
|
|
||||||
|
fireEvent.click(bucketsDropdownClosed)
|
||||||
|
|
||||||
|
const bucketItems = getAllByTestId(/dropdown--item/)
|
||||||
|
|
||||||
|
expect(bucketItems.length).toBe(2)
|
||||||
|
|
||||||
|
const b2 = getByTestId('dropdown--item b2')
|
||||||
|
|
||||||
|
fireEvent.click(b2)
|
||||||
|
|
||||||
|
const closedDropdown = await waitForElement(() =>
|
||||||
|
getByTestId('buckets--button')
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(closedDropdown.textContent).toBe('b2')
|
||||||
|
expect(queryAllByTestId(/dropdown--item/).length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can select a tag', async () => {
|
||||||
|
const {
|
||||||
|
getByText,
|
||||||
|
getByTestId,
|
||||||
|
getAllByTestId,
|
||||||
|
queryAllByTestId,
|
||||||
|
} = renderWithRedux(<QueryBuilder />, setInitialState)
|
||||||
|
|
||||||
|
let keysButton = await waitForElement(() => getByText('tk1'))
|
||||||
|
|
||||||
|
fireEvent.click(keysButton)
|
||||||
|
|
||||||
|
const keyMenuItems = getAllByTestId(/dropdown--item/)
|
||||||
|
|
||||||
|
expect(keyMenuItems.length).toBe(2)
|
||||||
|
|
||||||
|
const tk2 = getByTestId('dropdown--item tk2')
|
||||||
|
|
||||||
|
fireEvent.click(tk2)
|
||||||
|
|
||||||
|
keysButton = await waitForElement(() =>
|
||||||
|
getByTestId('tag-selector--dropdown-button')
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(keysButton.innerHTML.includes('tk2')).toBe(true)
|
||||||
|
expect(queryAllByTestId(/dropdown--item/).length).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can select a tag value', async () => {
|
||||||
|
const {getByText, getByTestId, queryAllByTestId} = renderWithRedux(
|
||||||
|
<QueryBuilder />,
|
||||||
|
setInitialState
|
||||||
|
)
|
||||||
|
|
||||||
|
const tagValue = await waitForElement(() => getByText('tv1'))
|
||||||
|
|
||||||
|
fireEvent.click(tagValue)
|
||||||
|
|
||||||
|
await waitForElement(() => getByTestId('tag-selector--container 1'))
|
||||||
|
|
||||||
|
expect(queryAllByTestId(/tag-selector--container/).length).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
|
@ -46,7 +46,7 @@ class TimeMachineQueryBuilder extends PureComponent<Props, State> {
|
||||||
const {tagFiltersLength} = this.props
|
const {tagFiltersLength} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="query-builder">
|
<div className="query-builder" data-testid="query-builder">
|
||||||
<div className="query-builder--buttons">
|
<div className="query-builder--buttons">
|
||||||
<Form.Element label="Bucket">
|
<Form.Element label="Bucket">
|
||||||
<QueryBuilderBucketDropdown />
|
<QueryBuilderBucketDropdown />
|
||||||
|
|
|
@ -39,6 +39,8 @@ const QueryBuilderBucketDropdown: SFC<Props> = props => {
|
||||||
onChange={bucket => onSelectBucket(bucket, true)}
|
onChange={bucket => onSelectBucket(bucket, true)}
|
||||||
buttonSize={ComponentSize.Small}
|
buttonSize={ComponentSize.Small}
|
||||||
status={toComponentStatus(bucketsStatus)}
|
status={toComponentStatus(bucketsStatus)}
|
||||||
|
testID="buckets"
|
||||||
|
buttonTestID="buckets--button"
|
||||||
>
|
>
|
||||||
{buckets.map(bucket => (
|
{buckets.map(bucket => (
|
||||||
<Dropdown.Item key={bucket} id={bucket} value={bucket}>
|
<Dropdown.Item key={bucket} id={bucket} value={bucket}>
|
||||||
|
|
|
@ -117,7 +117,10 @@ class TagSelector extends PureComponent<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="tag-selector--top">
|
<div
|
||||||
|
className="tag-selector--top"
|
||||||
|
data-testid={`tag-selector--container ${index}`}
|
||||||
|
>
|
||||||
<SearchableDropdown
|
<SearchableDropdown
|
||||||
searchTerm={keysSearchTerm}
|
searchTerm={keysSearchTerm}
|
||||||
searchPlaceholder="Search keys..."
|
searchPlaceholder="Search keys..."
|
||||||
|
@ -126,6 +129,8 @@ class TagSelector extends PureComponent<Props> {
|
||||||
onChange={this.handleSelectTag}
|
onChange={this.handleSelectTag}
|
||||||
status={toComponentStatus(keysStatus)}
|
status={toComponentStatus(keysStatus)}
|
||||||
titleText="No Tags Found"
|
titleText="No Tags Found"
|
||||||
|
testID="tag-selector--dropdown"
|
||||||
|
buttonTestID="tag-selector--dropdown-button"
|
||||||
>
|
>
|
||||||
{keys.map(key => (
|
{keys.map(key => (
|
||||||
<Dropdown.Item key={key} id={key} value={key}>
|
<Dropdown.Item key={key} id={key} value={key}>
|
||||||
|
|
|
@ -72,7 +72,16 @@ export const initialStateHelper = (): TimeMachineState => ({
|
||||||
queryBuilder: {
|
queryBuilder: {
|
||||||
buckets: [],
|
buckets: [],
|
||||||
bucketsStatus: RemoteDataState.NotStarted,
|
bucketsStatus: RemoteDataState.NotStarted,
|
||||||
tags: [],
|
tags: [
|
||||||
|
{
|
||||||
|
valuesSearchTerm: '',
|
||||||
|
keysSearchTerm: '',
|
||||||
|
keys: [],
|
||||||
|
keysStatus: RemoteDataState.NotStarted,
|
||||||
|
values: [],
|
||||||
|
valuesStatus: RemoteDataState.NotStarted,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -128,13 +137,15 @@ export const timeMachinesReducer = (
|
||||||
|
|
||||||
const newActiveTimeMachine = timeMachineReducer(activeTimeMachine, action)
|
const newActiveTimeMachine = timeMachineReducer(activeTimeMachine, action)
|
||||||
|
|
||||||
return {
|
const s = {
|
||||||
...state,
|
...state,
|
||||||
timeMachines: {
|
timeMachines: {
|
||||||
...timeMachines,
|
...timeMachines,
|
||||||
[activeTimeMachineID]: newActiveTimeMachine,
|
[activeTimeMachineID]: newActiveTimeMachine,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
export const timeMachineReducer = (
|
export const timeMachineReducer = (
|
||||||
|
@ -667,7 +678,10 @@ const setViewProperties = (
|
||||||
const view: any = state.view
|
const view: any = state.view
|
||||||
const properties = view.properties
|
const properties = view.properties
|
||||||
|
|
||||||
return {...state, view: {...view, properties: {...properties, ...update}}}
|
return {
|
||||||
|
...state,
|
||||||
|
view: {...view, properties: {...properties, ...update}},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setYAxis = (state: TimeMachineState, update: {[key: string]: any}) => {
|
const setYAxis = (state: TimeMachineState, update: {[key: string]: any}) => {
|
||||||
|
@ -680,7 +694,10 @@ const setYAxis = (state: TimeMachineState, update: {[key: string]: any}) => {
|
||||||
...state,
|
...state,
|
||||||
view: {
|
view: {
|
||||||
...view,
|
...view,
|
||||||
properties: {...properties, axes: {...axes, y: {...yAxis, ...update}}},
|
properties: {
|
||||||
|
...properties,
|
||||||
|
axes: {...axes, y: {...yAxis, ...update}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import {TimeRange} from 'src/types'
|
|
||||||
import {AppState} from 'src/shared/reducers/app'
|
import {AppState} from 'src/shared/reducers/app'
|
||||||
|
|
||||||
export interface LocalStorage {
|
export interface LocalStorage {
|
||||||
VERSION: string
|
VERSION: string
|
||||||
app: AppState
|
app: AppState
|
||||||
ranges: []
|
ranges: any[]
|
||||||
timeRange: TimeRange
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue