Fix query bug resulting from missing org ID

The `/api/v2/query` endpoint requires an organization or organizationID
query parameter.

Previously, there existed a bug in the API where if the organization
parameters were left off, the API would use the first organization
created in the backend, rather than returning a 400 error.

We built the entire UI on top of this bug, but it has now been fixed. So
in every location where we use the `/api/v2/query` endpoint, we need to
supply an organization.

This commit updates all such locations to use the first organization
present in our Redux store as the organization parameter, thus roughly
reproducing the behavior of the load bearing bug.

This is just a quick fix. Long term, we will want to think about what
organization queries should run under and build an appropriate UI around
that design.
pull/12084/head
Christopher Henn 2019-02-21 14:50:55 -08:00 committed by Chris Henn
parent 0cc05ee0e7
commit c664e8e0d8
9 changed files with 90 additions and 45 deletions

View File

@ -1,44 +1,39 @@
// Libraries
import React from 'react'
import {shallow} from 'enzyme'
// Components
import DataListening from 'src/dataLoaders/components/verifyStep/DataListening'
import ConnectionInformation from 'src/dataLoaders/components/verifyStep/ConnectionInformation'
import {Button} from '@influxdata/clockface'
const setup = (override = {}) => {
const props = {
bucket: 'defbuck',
stepIndex: 4,
...override,
// Utils
import {renderWithRedux} from 'src/mockState'
import {fireEvent} from 'react-testing-library'
const setInitialState = state => {
return {
...state,
orgs: [
{
id: 'foo',
},
],
}
const wrapper = shallow(<DataListening {...props} />)
return {wrapper}
}
describe('Onboarding.Components.DataListening', () => {
it('renders', () => {
const {wrapper} = setup()
const button = wrapper.find(Button)
expect(wrapper.exists()).toBe(true)
expect(button.exists()).toBe(true)
})
describe('if button is clicked', () => {
it('displays connection information', () => {
const {wrapper} = setup()
const {getByTitle, getByText} = renderWithRedux(
<DataListening bucket="bucket" />,
setInitialState
)
const button = wrapper.find(Button)
button.simulate('click')
const button = getByTitle('Listen for Data')
const connectionInfo = wrapper.find(ConnectionInformation)
fireEvent.click(button)
expect(wrapper.exists()).toBe(true)
expect(connectionInfo.exists()).toBe(true)
const message = getByText('Awaiting Connection...')
expect(message).toBeDefined()
})
})
})

View File

@ -1,9 +1,11 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
// Apis
import {executeQuery} from 'src/shared/apis/v2/query'
import {getActiveOrg} from 'src/organizations/selectors'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
@ -18,12 +20,19 @@ import ConnectionInformation, {
} from 'src/dataLoaders/components/verifyStep/ConnectionInformation'
// Types
import {AppState, Organization} from 'src/types/v2'
import {InfluxLanguage} from 'src/types/v2/dashboards'
export interface Props {
interface OwnProps {
bucket: string
}
interface StateProps {
activeOrg: Organization
}
type Props = OwnProps & StateProps
interface State {
loading: LoadingState
timePassedInSeconds: number
@ -112,7 +121,7 @@ class DataListening extends PureComponent<Props, State> {
}
private checkForData = async (): Promise<void> => {
const {bucket} = this.props
const {bucket, activeOrg} = this.props
const {secondsLeft} = this.state
const script = `from(bucket: "${bucket}")
|> range(start: -1m)`
@ -123,6 +132,7 @@ class DataListening extends PureComponent<Props, State> {
try {
const response = await executeQuery(
'/api/v2/query',
activeOrg.id,
script,
InfluxLanguage.Flux
).promise
@ -165,4 +175,11 @@ class DataListening extends PureComponent<Props, State> {
}
}
export default DataListening
const mstp = (state: AppState) => ({
activeOrg: getActiveOrg(state),
})
export default connect<StateProps, {}, OwnProps>(
mstp,
null
)(DataListening)

View File

@ -0,0 +1,3 @@
import {AppState, Organization} from 'src/types/v2'
export const getActiveOrg = (state: AppState): Organization => state.orgs[0]

View File

@ -20,6 +20,7 @@ interface XHRError extends Error {
export const executeQuery = (
url: string,
orgID: string,
query: string,
language: InfluxLanguage = InfluxLanguage.Flux
): WrappedCancelablePromise<ExecuteFluxQueryResult> => {
@ -127,7 +128,7 @@ export const executeQuery = (
const dialect = {annotations: ['group', 'datatype', 'default']}
const body = JSON.stringify({query, dialect, type: language})
xhr.open('POST', url)
xhr.open('POST', `${url}?orgID=${encodeURIComponent(orgID)}`)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(body)

View File

@ -10,18 +10,20 @@ import {executeQuery, ExecuteFluxQueryResult} from 'src/shared/apis/v2/query'
import {parseResponse} from 'src/shared/parsing/flux/response'
import {getSources, getActiveSource} from 'src/sources/selectors'
import {renderQuery} from 'src/shared/utils/renderQuery'
import {getActiveOrg} from 'src/organizations/selectors'
// Types
import {RemoteDataState, FluxTable} from 'src/types'
import {DashboardQuery} from 'src/types/v2/dashboards'
import {AppState, Source} from 'src/types/v2'
import {AppState, Source, Organization} from 'src/types/v2'
import {WrappedCancelablePromise, CancellationError} from 'src/types/promises'
type URLQuery = DashboardQuery & {url: string}
const executeRenderedQuery = (
{text, type, url}: URLQuery,
variables: {[key: string]: string}
variables: {[key: string]: string},
orgID: string
): WrappedCancelablePromise<ExecuteFluxQueryResult> => {
let isCancelled = false
let cancelExecution
@ -39,7 +41,7 @@ const executeRenderedQuery = (
return Promise.reject(new CancellationError())
}
const pendingResult = executeQuery(url, renderedQuery, type)
const pendingResult = executeQuery(url, orgID, renderedQuery, type)
cancelExecution = pendingResult.cancel
@ -61,6 +63,7 @@ export interface QueriesState {
interface StateProps {
dynamicSourceURL: string
sources: Source[]
activeOrg: Organization
}
interface OwnProps {
@ -141,7 +144,7 @@ class TimeSeries extends Component<Props, State> {
}
private reload = async () => {
const {inView, variables} = this.props
const {inView, variables, activeOrg} = this.props
const queries = this.queries
if (!inView) {
@ -167,7 +170,9 @@ class TimeSeries extends Component<Props, State> {
this.pendingResults.forEach(({cancel}) => cancel())
// Issue new queries
this.pendingResults = queries.map(q => executeRenderedQuery(q, variables))
this.pendingResults = queries.map(q =>
executeRenderedQuery(q, variables, activeOrg.id)
)
// Wait for new queries to complete
const results = await Promise.all(this.pendingResults.map(r => r.promise))
@ -218,8 +223,9 @@ class TimeSeries extends Component<Props, State> {
const mstp = (state: AppState) => {
const sources = getSources(state)
const dynamicSourceURL = getActiveSource(state).links.query
const activeOrg = getActiveOrg(state)
return {sources, dynamicSourceURL}
return {sources, dynamicSourceURL, activeOrg}
}
export default connect<StateProps, {}, OwnProps>(

View File

@ -2,6 +2,7 @@
import {queryBuilderFetcher} from 'src/timeMachine/apis/QueryBuilderFetcher'
// Utils
import {getActiveOrg} from 'src/organizations/selectors'
import {
getActiveQuerySource,
getActiveQuery,
@ -203,11 +204,13 @@ export const loadBuckets = () => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
const queryURL = getActiveQuerySource(getState()).links.query
const orgID = getActiveOrg(getState()).id
dispatch(setBuilderBucketsStatus(RemoteDataState.Loading))
try {
const queryURL = getActiveQuerySource(getState()).links.query
const buckets = await queryBuilderFetcher.findBuckets(queryURL)
const buckets = await queryBuilderFetcher.findBuckets(queryURL, orgID)
const selectedBucket = getActiveQuery(getState()).builderConfig.buckets[0]
dispatch(setBuilderBuckets(buckets))
@ -247,6 +250,7 @@ export const loadTagSelector = (index: number) => async (
const tagPredicates = tags.slice(0, index)
const queryURL = getActiveQuerySource(getState()).links.query
const orgID = getActiveOrg(getState()).id
dispatch(setBuilderTagKeysStatus(index, RemoteDataState.Loading))
@ -257,6 +261,7 @@ export const loadTagSelector = (index: number) => async (
const keys = await queryBuilderFetcher.findKeys(
index,
queryURL,
orgID,
buckets[0],
tagPredicates,
searchTerm
@ -299,6 +304,7 @@ const loadTagSelectorValues = (index: number) => async (
const {buckets, tags} = getActiveQuery(getState()).builderConfig
const tagPredicates = tags.slice(0, index)
const queryURL = getActiveQuerySource(getState()).links.query
const orgID = getActiveOrg(getState()).id
dispatch(setBuilderTagValuesStatus(index, RemoteDataState.Loading))
@ -309,6 +315,7 @@ const loadTagSelectorValues = (index: number) => async (
const values = await queryBuilderFetcher.findValues(
index,
queryURL,
orgID,
buckets[0],
tagPredicates,
key,

View File

@ -19,7 +19,7 @@ class QueryBuilderFetcher {
private findValuesCache: {[key: string]: string[]} = {}
private findBucketsCache: {[key: string]: string[]} = {}
public async findBuckets(url: string): Promise<string[]> {
public async findBuckets(url: string, orgID: string): Promise<string[]> {
this.cancelFindBuckets()
const cacheKey = JSON.stringify([...arguments])
@ -29,7 +29,7 @@ class QueryBuilderFetcher {
return Promise.resolve(cachedResult)
}
const pendingResult = findBuckets(url)
const pendingResult = findBuckets(url, orgID)
pendingResult.promise.then(result => {
this.findBucketsCache[cacheKey] = result
@ -47,6 +47,7 @@ class QueryBuilderFetcher {
public async findKeys(
index: number,
url: string,
orgID: string,
bucket: string,
tagsSelections: BuilderConfig['tags'],
searchTerm: string = ''
@ -60,7 +61,13 @@ class QueryBuilderFetcher {
return Promise.resolve(cachedResult)
}
const pendingResult = findKeys(url, bucket, tagsSelections, searchTerm)
const pendingResult = findKeys(
url,
orgID,
bucket,
tagsSelections,
searchTerm
)
this.findKeysQueries[index] = pendingResult
@ -80,6 +87,7 @@ class QueryBuilderFetcher {
public async findValues(
index: number,
url: string,
orgID: string,
bucket: string,
tagsSelections: BuilderConfig['tags'],
key: string,
@ -96,6 +104,7 @@ class QueryBuilderFetcher {
const pendingResult = findValues(
url,
orgID,
bucket,
tagsSelections,
key,

View File

@ -14,12 +14,12 @@ export const LIMIT = 200
type CancelableQuery = WrappedCancelablePromise<string[]>
export function findBuckets(url: string): CancelableQuery {
export function findBuckets(url: string, orgID: string): CancelableQuery {
const query = `buckets()
|> sort(columns: ["name"])
|> limit(n: ${LIMIT})`
const {promise, cancel} = executeQuery(url, query, InfluxLanguage.Flux)
const {promise, cancel} = executeQuery(url, orgID, query, InfluxLanguage.Flux)
return {
promise: promise.then(resp => extractCol(resp, 'name')),
@ -29,6 +29,7 @@ export function findBuckets(url: string): CancelableQuery {
export function findKeys(
url: string,
orgID: string,
bucket: string,
tagsSelections: BuilderConfig['tags'],
searchTerm: string = ''
@ -49,7 +50,7 @@ v1.tagKeys(bucket: "${bucket}", predicate: ${tagFilters}, start: -${SEARCH_DURAT
|> sort()
|> limit(n: ${LIMIT})`
const {promise, cancel} = executeQuery(url, query, InfluxLanguage.Flux)
const {promise, cancel} = executeQuery(url, orgID, query, InfluxLanguage.Flux)
return {
promise: promise.then(resp => extractCol(resp, '_value')),
@ -59,6 +60,7 @@ v1.tagKeys(bucket: "${bucket}", predicate: ${tagFilters}, start: -${SEARCH_DURAT
export function findValues(
url: string,
orgID: string,
bucket: string,
tagsSelections: BuilderConfig['tags'],
key: string,
@ -73,7 +75,7 @@ v1.tagValues(bucket: "${bucket}", tag: "${key}", predicate: ${tagFilters}, start
|> limit(n: ${LIMIT})
|> sort()`
const {promise, cancel} = executeQuery(url, query, InfluxLanguage.Flux)
const {promise, cancel} = executeQuery(url, orgID, query, InfluxLanguage.Flux)
return {
promise: promise.then(resp => extractCol(resp, '_value')),

View File

@ -17,6 +17,11 @@ const setInitialState = state => {
[source.id]: source,
},
},
orgs: [
{
id: 'foo',
},
],
}
}