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 {
|
||||
Source,
|
||||
SourceAuthenticationMethod,
|
||||
Template,
|
||||
SourceLinks,
|
||||
TemplateType,
|
||||
TemplateValueType,
|
||||
} from 'src/types'
|
||||
import {Template, SourceLinks, TemplateType, TemplateValueType} from 'src/types'
|
||||
import {Source} from 'src/api'
|
||||
import {Cell, Dashboard, Label} from 'src/types/v2'
|
||||
import {Links} from 'src/types/v2/links'
|
||||
import {Task} from 'src/types/v2/tasks'
|
||||
|
@ -123,14 +117,12 @@ export const sourceLinks: SourceLinks = {
|
|||
export const source: Source = {
|
||||
id: '16',
|
||||
name: 'ssl',
|
||||
type: 'influx',
|
||||
type: Source.TypeEnum.Self,
|
||||
username: 'admin',
|
||||
url: 'https://localhost:9086',
|
||||
insecureSkipVerify: true,
|
||||
default: false,
|
||||
telegraf: 'telegraf',
|
||||
links: sourceLinks,
|
||||
authentication: SourceAuthenticationMethod.Basic,
|
||||
}
|
||||
|
||||
export const timeRange = {
|
||||
|
@ -583,7 +575,11 @@ export const setSetupParamsResponse = {
|
|||
userID: '033bc62520fe3000',
|
||||
user: 'iris',
|
||||
permissions: [
|
||||
{action: 'read', resource: 'authorizations', orgID: '033bc62534be3000'},
|
||||
{
|
||||
action: 'read',
|
||||
resource: 'authorizations',
|
||||
orgID: '033bc62534be3000',
|
||||
},
|
||||
{
|
||||
action: 'write',
|
||||
resource: 'authorizations',
|
||||
|
|
|
@ -902,6 +902,12 @@
|
|||
"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": {
|
||||
"version": "5.0.1",
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
|
||||
|
@ -10629,6 +10647,15 @@
|
|||
"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": {
|
||||
"version": "3.8.4",
|
||||
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-3.8.4.tgz",
|
||||
|
@ -13000,6 +13027,12 @@
|
|||
"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": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz",
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"tsc:watch": "tsc -p ./tsconfig.json --noEmit --pretty -w"
|
||||
},
|
||||
"jest": {
|
||||
"setupTestFrameworkScriptFile": "./jestSetup.ts",
|
||||
"displayName": "test",
|
||||
"testURL": "http://localhost",
|
||||
"testPathIgnorePatterns": [
|
||||
|
@ -104,6 +105,7 @@
|
|||
"jsdom": "^9.0.0",
|
||||
"parcel": "^1.11.0",
|
||||
"prettier": "^1.14.3",
|
||||
"react-testing-library": "^5.4.4",
|
||||
"sass": "^1.15.3",
|
||||
"ts-jest": "^23.10.3",
|
||||
"tslib": "^1.9.0",
|
||||
|
|
|
@ -43,6 +43,8 @@ export interface Props {
|
|||
mode?: DropdownMode
|
||||
titleText?: string
|
||||
menuHeader?: JSX.Element
|
||||
testID: string
|
||||
buttonTestID: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -122,6 +124,7 @@ class Dropdown extends Component<Props, State> {
|
|||
icon,
|
||||
children,
|
||||
titleText,
|
||||
buttonTestID,
|
||||
} = this.props
|
||||
const {expanded} = this.state
|
||||
|
||||
|
@ -147,6 +150,7 @@ class Dropdown extends Component<Props, State> {
|
|||
onClick={this.toggleMenu}
|
||||
status={status}
|
||||
title={titleText}
|
||||
testID={buttonTestID}
|
||||
>
|
||||
{dropdownLabel}
|
||||
</DropdownButton>
|
||||
|
@ -160,6 +164,7 @@ class Dropdown extends Component<Props, State> {
|
|||
menuHeader,
|
||||
menuColor,
|
||||
children,
|
||||
testID,
|
||||
} = this.props
|
||||
const {expanded} = this.state
|
||||
|
||||
|
@ -174,7 +179,10 @@ class Dropdown extends Component<Props, State> {
|
|||
autoHeight={true}
|
||||
maxHeight={maxMenuHeight}
|
||||
>
|
||||
<div className="dropdown--menu">
|
||||
<div
|
||||
className="dropdown--menu"
|
||||
data-testid={`dropdown--menu ${testID}`}
|
||||
>
|
||||
{menuHeader && menuHeader}
|
||||
{React.Children.map(children, (child: JSX.Element) => {
|
||||
if (this.childTypeIsValid(child)) {
|
||||
|
|
|
@ -23,6 +23,7 @@ interface Props {
|
|||
size?: ComponentSize
|
||||
icon?: IconFont
|
||||
title?: string
|
||||
testID: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -35,7 +36,7 @@ class DropdownButton extends Component<Props> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {onClick, children, title} = this.props
|
||||
const {onClick, children, title, testID} = this.props
|
||||
return (
|
||||
<button
|
||||
className={this.classname}
|
||||
|
@ -43,6 +44,7 @@ class DropdownButton extends Component<Props> {
|
|||
disabled={this.isDisabled}
|
||||
title={title}
|
||||
type={ButtonType.Button}
|
||||
data-testid={testID}
|
||||
>
|
||||
{this.icon}
|
||||
<span className="dropdown--selected">{children}</span>
|
||||
|
|
|
@ -14,6 +14,7 @@ interface Props {
|
|||
selected?: boolean
|
||||
checkbox?: boolean
|
||||
onClick?: (value: any) => void
|
||||
testid?: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -24,7 +25,7 @@ class DropdownItem extends Component<Props> {
|
|||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const {selected, checkbox} = this.props
|
||||
const {selected, checkbox, id} = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -33,6 +34,7 @@ class DropdownItem extends Component<Props> {
|
|||
active: selected && !checkbox,
|
||||
'multi-select--item': checkbox,
|
||||
})}
|
||||
data-testid={`dropdown--item ${id}`}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{this.checkBox}
|
||||
|
|
|
@ -11,7 +11,7 @@ import {setActiveTimeMachine} from 'src/timeMachine/actions'
|
|||
// Utils
|
||||
import {DE_TIME_MACHINE_ID} from 'src/timeMachine/constants'
|
||||
import {HoverTimeProvider} from 'src/dashboards/utils/hoverTime'
|
||||
import {queryBuilderFetcher} from 'src/timeMachine/apis/queryBuilder'
|
||||
import {queryBuilderFetcher} from 'src/timeMachine/apis/QueryBuilderFetcher'
|
||||
|
||||
// Styles
|
||||
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() {
|
||||
const {searchTerm, searchPlaceholder, buttonSize} = this.props
|
||||
const {searchTerm, searchPlaceholder, buttonSize, testID} = this.props
|
||||
|
||||
const dropdownProps = omit(this.props, [
|
||||
'searchTerm',
|
||||
|
@ -50,6 +50,7 @@ export default class SearchableDropdown extends Component<Props> {
|
|||
autoFocus={true}
|
||||
/>
|
||||
}
|
||||
testID={testID}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -28,12 +28,9 @@ import protosReducer from 'src/protos/reducers'
|
|||
import {LocalStorage} from 'src/types/localStorage'
|
||||
import {AppState} from 'src/types/v2'
|
||||
|
||||
type ReducerState = Pick<
|
||||
AppState,
|
||||
Exclude<keyof AppState, 'VERSION' | 'timeRange'>
|
||||
>
|
||||
type ReducerState = Pick<AppState, Exclude<keyof AppState, 'timeRange'>>
|
||||
|
||||
const rootReducer = combineReducers<ReducerState>({
|
||||
export const rootReducer = combineReducers<ReducerState>({
|
||||
...sharedReducers,
|
||||
ranges: rangesReducer,
|
||||
dashboards: dashboardsReducer,
|
||||
|
@ -49,6 +46,7 @@ const rootReducer = combineReducers<ReducerState>({
|
|||
noteEditor: noteEditorReducer,
|
||||
dataLoading: dataLoadingReducer,
|
||||
protos: protosReducer,
|
||||
VERSION: () => '',
|
||||
})
|
||||
|
||||
const composeEnhancers =
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// APIs
|
||||
import {queryBuilderFetcher} from 'src/timeMachine/apis/queryBuilder'
|
||||
import {queryBuilderFetcher} from 'src/timeMachine/apis/QueryBuilderFetcher'
|
||||
|
||||
// Utils
|
||||
import {
|
||||
|
@ -47,7 +47,9 @@ interface SetBuilderBucketsAction {
|
|||
payload: {buckets: string[]}
|
||||
}
|
||||
|
||||
const setBuilderBuckets = (buckets: string[]): SetBuilderBucketsAction => ({
|
||||
export const setBuilderBuckets = (
|
||||
buckets: string[]
|
||||
): SetBuilderBucketsAction => ({
|
||||
type: 'SET_BUILDER_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[]>
|
||||
|
||||
function findBuckets(url: string): CancelableQuery {
|
||||
export function findBuckets(url: string): CancelableQuery {
|
||||
const query = `buckets()
|
||||
|> sort(columns: ["name"])
|
||||
|> limit(n: ${LIMIT})`
|
||||
|
@ -27,7 +27,7 @@ function findBuckets(url: string): CancelableQuery {
|
|||
}
|
||||
}
|
||||
|
||||
function findKeys(
|
||||
export function findKeys(
|
||||
url: string,
|
||||
bucket: string,
|
||||
tagsSelections: BuilderConfig['tags'],
|
||||
|
@ -57,7 +57,7 @@ v1.tagKeys(bucket: "${bucket}", predicate: ${tagFilters}, start: -${SEARCH_DURAT
|
|||
}
|
||||
}
|
||||
|
||||
function findValues(
|
||||
export function findValues(
|
||||
url: string,
|
||||
bucket: string,
|
||||
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 data = get(tables, '0.data', [])
|
||||
|
||||
|
@ -104,7 +107,9 @@ function extractCol(resp: ExecuteFluxQueryResult, colName: string): string[] {
|
|||
return colValues
|
||||
}
|
||||
|
||||
function formatTagFilterPredicate(tagsSelections: BuilderConfig['tags']) {
|
||||
export function formatTagFilterPredicate(
|
||||
tagsSelections: BuilderConfig['tags']
|
||||
) {
|
||||
const validSelections = tagsSelections.filter(
|
||||
({key, values}) => key && values.length
|
||||
)
|
||||
|
@ -124,7 +129,7 @@ function formatTagFilterPredicate(tagsSelections: BuilderConfig['tags']) {
|
|||
return `(r) => ${calls}`
|
||||
}
|
||||
|
||||
function formatTagKeyFilterCall(tagsSelections: BuilderConfig['tags']) {
|
||||
export function formatTagKeyFilterCall(tagsSelections: BuilderConfig['tags']) {
|
||||
const keys = tagsSelections.map(({key}) => key)
|
||||
|
||||
if (!keys.length) {
|
||||
|
@ -136,125 +141,10 @@ function formatTagKeyFilterCall(tagsSelections: BuilderConfig['tags']) {
|
|||
return `\n |> filter(fn: (r) => ${fnBody})`
|
||||
}
|
||||
|
||||
function formatSearchFilterCall(searchTerm: string) {
|
||||
export function formatSearchFilterCall(searchTerm: string) {
|
||||
if (!searchTerm) {
|
||||
return ''
|
||||
}
|
||||
|
||||
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(
|
||||
state
|
||||
)
|
||||
|
||||
const activeQuery = getActiveQuery(state)
|
||||
|
||||
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
|
||||
|
||||
return (
|
||||
<div className="query-builder">
|
||||
<div className="query-builder" data-testid="query-builder">
|
||||
<div className="query-builder--buttons">
|
||||
<Form.Element label="Bucket">
|
||||
<QueryBuilderBucketDropdown />
|
||||
|
|
|
@ -39,6 +39,8 @@ const QueryBuilderBucketDropdown: SFC<Props> = props => {
|
|||
onChange={bucket => onSelectBucket(bucket, true)}
|
||||
buttonSize={ComponentSize.Small}
|
||||
status={toComponentStatus(bucketsStatus)}
|
||||
testID="buckets"
|
||||
buttonTestID="buckets--button"
|
||||
>
|
||||
{buckets.map(bucket => (
|
||||
<Dropdown.Item key={bucket} id={bucket} value={bucket}>
|
||||
|
|
|
@ -117,7 +117,10 @@ class TagSelector extends PureComponent<Props> {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="tag-selector--top">
|
||||
<div
|
||||
className="tag-selector--top"
|
||||
data-testid={`tag-selector--container ${index}`}
|
||||
>
|
||||
<SearchableDropdown
|
||||
searchTerm={keysSearchTerm}
|
||||
searchPlaceholder="Search keys..."
|
||||
|
@ -126,6 +129,8 @@ class TagSelector extends PureComponent<Props> {
|
|||
onChange={this.handleSelectTag}
|
||||
status={toComponentStatus(keysStatus)}
|
||||
titleText="No Tags Found"
|
||||
testID="tag-selector--dropdown"
|
||||
buttonTestID="tag-selector--dropdown-button"
|
||||
>
|
||||
{keys.map(key => (
|
||||
<Dropdown.Item key={key} id={key} value={key}>
|
||||
|
|
|
@ -72,7 +72,16 @@ export const initialStateHelper = (): TimeMachineState => ({
|
|||
queryBuilder: {
|
||||
buckets: [],
|
||||
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)
|
||||
|
||||
return {
|
||||
const s = {
|
||||
...state,
|
||||
timeMachines: {
|
||||
...timeMachines,
|
||||
[activeTimeMachineID]: newActiveTimeMachine,
|
||||
},
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
export const timeMachineReducer = (
|
||||
|
@ -667,7 +678,10 @@ const setViewProperties = (
|
|||
const view: any = state.view
|
||||
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}) => {
|
||||
|
@ -680,7 +694,10 @@ const setYAxis = (state: TimeMachineState, update: {[key: string]: any}) => {
|
|||
...state,
|
||||
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'
|
||||
|
||||
export interface LocalStorage {
|
||||
VERSION: string
|
||||
app: AppState
|
||||
ranges: []
|
||||
timeRange: TimeRange
|
||||
ranges: any[]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue