Implement new visual query builder designs
parent
5de0e2e8d2
commit
2770ae5e7b
|
@ -6656,6 +6656,11 @@
|
|||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz",
|
||||
"integrity": "sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw="
|
||||
},
|
||||
"immer": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-1.9.3.tgz",
|
||||
"integrity": "sha512-bUyz3fOHGn82V7h4oVgJGmFglZt53JWwSyVNAT4sO0d7IovHLwLuHbh14uYKY0tewFoDcEdiQW7HuL0NsRVziw=="
|
||||
},
|
||||
"import-fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
|
||||
|
|
|
@ -123,6 +123,7 @@
|
|||
"encoding-down": "^5.0.4",
|
||||
"fast.js": "^0.1.1",
|
||||
"history": "^3.2",
|
||||
"immer": "^1.9.3",
|
||||
"level-js": "^3.0.0",
|
||||
"levelup": "^3.1.1",
|
||||
"lodash": "^4.3.0",
|
||||
|
|
|
@ -9,6 +9,7 @@ import DropdownDivider from 'src/clockface/components/dropdowns/DropdownDivider'
|
|||
import DropdownItem from 'src/clockface/components/dropdowns/DropdownItem'
|
||||
import DropdownButton from 'src/clockface/components/dropdowns/DropdownButton'
|
||||
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
|
||||
import WaitingText from 'src/shared/components/WaitingText'
|
||||
|
||||
// Types
|
||||
import {
|
||||
|
@ -124,8 +125,17 @@ class Dropdown extends Component<Props, State> {
|
|||
const {expanded} = this.state
|
||||
|
||||
const selectedChild = children.find(child => child.props.id === selectedID)
|
||||
const dropdownLabel =
|
||||
(selectedChild && selectedChild.props.children) || titleText
|
||||
const isLoading = status === ComponentStatus.Loading
|
||||
|
||||
let dropdownLabel
|
||||
|
||||
if (isLoading) {
|
||||
dropdownLabel = <WaitingText text="Loading" />
|
||||
} else if (selectedChild) {
|
||||
dropdownLabel = selectedChild.props.children
|
||||
} else {
|
||||
dropdownLabel = titleText
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownButton
|
||||
|
@ -211,6 +221,14 @@ class Dropdown extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
private get shouldHaveChildren(): boolean {
|
||||
const {status} = this.props
|
||||
|
||||
return (
|
||||
status === ComponentStatus.Default || status === ComponentStatus.Valid
|
||||
)
|
||||
}
|
||||
|
||||
private handleItemClick = (value: any): void => {
|
||||
const {onChange} = this.props
|
||||
onChange(value)
|
||||
|
@ -220,7 +238,7 @@ class Dropdown extends Component<Props, State> {
|
|||
private validateChildCount = (): void => {
|
||||
const {children} = this.props
|
||||
|
||||
if (React.Children.count(children) === 0) {
|
||||
if (this.shouldHaveChildren && React.Children.count(children) === 0) {
|
||||
throw new Error(
|
||||
'Dropdowns require at least 1 child element. We recommend using Dropdown.Item and/or Dropdown.Divider.'
|
||||
)
|
||||
|
@ -236,6 +254,7 @@ class Dropdown extends Component<Props, State> {
|
|||
|
||||
if (
|
||||
mode === DropdownMode.Radio &&
|
||||
this.shouldHaveChildren &&
|
||||
(isUndefined(selectedID) || isNull(selectedID))
|
||||
) {
|
||||
throw new Error('Dropdowns in Radio mode require a selectedID prop.')
|
||||
|
|
|
@ -34,29 +34,51 @@ class DropdownButton extends Component<Props> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {onClick, status, children, title} = this.props
|
||||
const {onClick, children, title} = this.props
|
||||
return (
|
||||
<button
|
||||
className={this.classname}
|
||||
onClick={onClick}
|
||||
disabled={status === ComponentStatus.Disabled}
|
||||
disabled={this.isDisabled}
|
||||
title={title}
|
||||
>
|
||||
{this.icon}
|
||||
<span className="dropdown--selected">{children}</span>
|
||||
<span className="dropdown--caret icon caret-down" />
|
||||
{this.caret}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
private get caret(): JSX.Element {
|
||||
const {active} = this.props
|
||||
|
||||
if (active) {
|
||||
return <span className="dropdown--caret icon caret-up" />
|
||||
}
|
||||
|
||||
return <span className="dropdown--caret icon caret-down" />
|
||||
}
|
||||
|
||||
private get isDisabled(): boolean {
|
||||
const {status} = this.props
|
||||
|
||||
const isDisabled = [
|
||||
ComponentStatus.Disabled,
|
||||
ComponentStatus.Loading,
|
||||
ComponentStatus.Error,
|
||||
].includes(status)
|
||||
|
||||
return isDisabled
|
||||
}
|
||||
|
||||
private get classname(): string {
|
||||
const {status, active, color, size} = this.props
|
||||
const {active, color, size} = this.props
|
||||
|
||||
return classnames('dropdown--button button', {
|
||||
'button-stretch': true,
|
||||
'button-disabled': this.isDisabled,
|
||||
[`button-${color}`]: color,
|
||||
[`button-${size}`]: size,
|
||||
disabled: status === ComponentStatus.Disabled,
|
||||
active,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -142,8 +142,7 @@ export const query: DashboardQuery = {
|
|||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
tags: [],
|
||||
functions: [],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,348 @@
|
|||
// APIs
|
||||
import {
|
||||
QueryBuilderFetcher,
|
||||
CancellationError,
|
||||
} from 'src/shared/apis/v2/queryBuilder'
|
||||
|
||||
// Utils
|
||||
import {
|
||||
getActiveQuerySource,
|
||||
getActiveQuery,
|
||||
} from 'src/shared/selectors/timeMachines'
|
||||
|
||||
// Types
|
||||
import {Dispatch} from 'redux-thunk'
|
||||
import {GetState} from 'src/types/v2'
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
||||
const fetcher = new QueryBuilderFetcher()
|
||||
|
||||
export type Action =
|
||||
| SetBuilderBucketSelectionAction
|
||||
| SetBuilderBucketsAction
|
||||
| SetBuilderBucketsStatusAction
|
||||
| SetBuilderTagKeysAction
|
||||
| SetBuilderTagKeysStatusAction
|
||||
| SetBuilderTagValuesAction
|
||||
| SetBuilderTagValuesStatusAction
|
||||
| SetBuilderTagKeySelectionAction
|
||||
| SetBuilderTagValuesSelectionAction
|
||||
| AddTagSelectorAction
|
||||
| RemoveTagSelectorAction
|
||||
| SelectFunctionAction
|
||||
|
||||
interface SetBuilderBucketsStatusAction {
|
||||
type: 'SET_BUILDER_BUCKETS_STATUS'
|
||||
payload: {bucketsStatus: RemoteDataState}
|
||||
}
|
||||
|
||||
const setBuilderBucketsStatus = (
|
||||
bucketsStatus: RemoteDataState
|
||||
): SetBuilderBucketsStatusAction => ({
|
||||
type: 'SET_BUILDER_BUCKETS_STATUS',
|
||||
payload: {bucketsStatus},
|
||||
})
|
||||
|
||||
interface SetBuilderBucketsAction {
|
||||
type: 'SET_BUILDER_BUCKETS'
|
||||
payload: {buckets: string[]}
|
||||
}
|
||||
|
||||
const setBuilderBuckets = (buckets: string[]): SetBuilderBucketsAction => ({
|
||||
type: 'SET_BUILDER_BUCKETS',
|
||||
payload: {buckets},
|
||||
})
|
||||
|
||||
interface SetBuilderBucketSelectionAction {
|
||||
type: 'SET_BUILDER_BUCKET_SELECTION'
|
||||
payload: {bucket: string}
|
||||
}
|
||||
|
||||
const setBuilderBucket = (bucket: string): SetBuilderBucketSelectionAction => ({
|
||||
type: 'SET_BUILDER_BUCKET_SELECTION',
|
||||
payload: {bucket},
|
||||
})
|
||||
|
||||
interface SetBuilderTagKeysAction {
|
||||
type: 'SET_BUILDER_TAG_KEYS'
|
||||
payload: {index: number; keys: string[]}
|
||||
}
|
||||
|
||||
const setBuilderTagKeys = (
|
||||
index: number,
|
||||
keys: string[]
|
||||
): SetBuilderTagKeysAction => ({
|
||||
type: 'SET_BUILDER_TAG_KEYS',
|
||||
payload: {index, keys},
|
||||
})
|
||||
|
||||
interface SetBuilderTagKeysStatusAction {
|
||||
type: 'SET_BUILDER_TAG_KEYS_STATUS'
|
||||
payload: {index: number; status: RemoteDataState}
|
||||
}
|
||||
|
||||
const setBuilderTagKeysStatus = (
|
||||
index: number,
|
||||
status: RemoteDataState
|
||||
): SetBuilderTagKeysStatusAction => ({
|
||||
type: 'SET_BUILDER_TAG_KEYS_STATUS',
|
||||
payload: {index, status},
|
||||
})
|
||||
|
||||
interface SetBuilderTagValuesAction {
|
||||
type: 'SET_BUILDER_TAG_VALUES'
|
||||
payload: {index: number; values: string[]}
|
||||
}
|
||||
|
||||
const setBuilderTagValues = (
|
||||
index: number,
|
||||
values: string[]
|
||||
): SetBuilderTagValuesAction => ({
|
||||
type: 'SET_BUILDER_TAG_VALUES',
|
||||
payload: {index, values},
|
||||
})
|
||||
|
||||
interface SetBuilderTagValuesStatusAction {
|
||||
type: 'SET_BUILDER_TAG_VALUES_STATUS'
|
||||
payload: {index: number; status: RemoteDataState}
|
||||
}
|
||||
|
||||
const setBuilderTagValuesStatus = (
|
||||
index: number,
|
||||
status: RemoteDataState
|
||||
): SetBuilderTagValuesStatusAction => ({
|
||||
type: 'SET_BUILDER_TAG_VALUES_STATUS',
|
||||
payload: {index, status},
|
||||
})
|
||||
|
||||
interface SetBuilderTagKeySelectionAction {
|
||||
type: 'SET_BUILDER_TAG_KEY_SELECTION'
|
||||
payload: {index: number; key: string}
|
||||
}
|
||||
|
||||
const setBuilderTagKeySelection = (
|
||||
index: number,
|
||||
key: string
|
||||
): SetBuilderTagKeySelectionAction => ({
|
||||
type: 'SET_BUILDER_TAG_KEY_SELECTION',
|
||||
payload: {index, key},
|
||||
})
|
||||
|
||||
interface SetBuilderTagValuesSelectionAction {
|
||||
type: 'SET_BUILDER_TAG_VALUES_SELECTION'
|
||||
payload: {index: number; values: string[]}
|
||||
}
|
||||
|
||||
const setBuilderTagValuesSelection = (
|
||||
index: number,
|
||||
values: string[]
|
||||
): SetBuilderTagValuesSelectionAction => ({
|
||||
type: 'SET_BUILDER_TAG_VALUES_SELECTION',
|
||||
payload: {index, values},
|
||||
})
|
||||
|
||||
interface AddTagSelectorAction {
|
||||
type: 'ADD_TAG_SELECTOR'
|
||||
}
|
||||
|
||||
const addTagSelectorSync = (): AddTagSelectorAction => ({
|
||||
type: 'ADD_TAG_SELECTOR',
|
||||
})
|
||||
|
||||
interface RemoveTagSelectorAction {
|
||||
type: 'REMOVE_TAG_SELECTOR'
|
||||
payload: {index: number}
|
||||
}
|
||||
|
||||
const removeTagSelectorSync = (index: number): RemoveTagSelectorAction => ({
|
||||
type: 'REMOVE_TAG_SELECTOR',
|
||||
payload: {index},
|
||||
})
|
||||
|
||||
interface SelectFunctionAction {
|
||||
type: 'SELECT_BUILDER_FUNCTION'
|
||||
payload: {name: string}
|
||||
}
|
||||
|
||||
export const selectFunction = (name: string): SelectFunctionAction => ({
|
||||
type: 'SELECT_BUILDER_FUNCTION',
|
||||
payload: {name},
|
||||
})
|
||||
|
||||
export const loadBuckets = () => async (
|
||||
dispatch: Dispatch<Action>,
|
||||
getState: GetState
|
||||
) => {
|
||||
const queryURL = getActiveQuerySource(getState()).links.query
|
||||
|
||||
dispatch(setBuilderBucketsStatus(RemoteDataState.Loading))
|
||||
|
||||
try {
|
||||
const buckets = await fetcher.findBuckets(queryURL)
|
||||
const selectedBucket = getActiveQuery(getState()).builderConfig.buckets[0]
|
||||
|
||||
dispatch(setBuilderBuckets(buckets))
|
||||
|
||||
if (selectedBucket && buckets.includes(selectedBucket)) {
|
||||
dispatch(selectBucket(selectedBucket))
|
||||
} else {
|
||||
dispatch(selectBucket(buckets[0]))
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof CancellationError) {
|
||||
return
|
||||
}
|
||||
|
||||
console.error(e)
|
||||
dispatch(setBuilderBucketsStatus(RemoteDataState.Error))
|
||||
}
|
||||
}
|
||||
|
||||
export const selectBucket = (bucket: string) => async (
|
||||
dispatch: Dispatch<Action>
|
||||
) => {
|
||||
dispatch(setBuilderBucket(bucket))
|
||||
dispatch(loadTagSelector(0))
|
||||
}
|
||||
|
||||
export const loadTagSelector = (index: number) => async (
|
||||
dispatch: Dispatch<Action>,
|
||||
getState: GetState
|
||||
) => {
|
||||
const {buckets, tags} = getActiveQuery(getState()).builderConfig
|
||||
|
||||
if (!tags[index] || !buckets[0]) {
|
||||
return
|
||||
}
|
||||
|
||||
const tagPredicates = tags.slice(0, index)
|
||||
const queryURL = getActiveQuerySource(getState()).links.query
|
||||
|
||||
dispatch(setBuilderTagKeysStatus(index, RemoteDataState.Loading))
|
||||
|
||||
try {
|
||||
const keys = await fetcher.findKeys(
|
||||
index,
|
||||
queryURL,
|
||||
buckets[0],
|
||||
tagPredicates
|
||||
)
|
||||
|
||||
const {key} = tags[index]
|
||||
|
||||
if (!key) {
|
||||
dispatch(setBuilderTagKeySelection(index, keys[0]))
|
||||
} else if (!keys.includes(key)) {
|
||||
// Even if the selected key didn't come back in the results, let it be
|
||||
// selected anyway
|
||||
keys.unshift(key)
|
||||
}
|
||||
|
||||
dispatch(setBuilderTagKeys(index, keys))
|
||||
dispatch(loadTagSelectorValues(index))
|
||||
} catch (e) {
|
||||
if (e instanceof CancellationError) {
|
||||
return
|
||||
}
|
||||
|
||||
console.error(e)
|
||||
dispatch(setBuilderTagKeysStatus(index, RemoteDataState.Error))
|
||||
}
|
||||
}
|
||||
|
||||
const loadTagSelectorValues = (index: number) => async (
|
||||
dispatch: Dispatch<Action>,
|
||||
getState: GetState
|
||||
) => {
|
||||
const {buckets, tags} = getActiveQuery(getState()).builderConfig
|
||||
const tagPredicates = tags.slice(0, index)
|
||||
const queryURL = getActiveQuerySource(getState()).links.query
|
||||
|
||||
dispatch(setBuilderTagValuesStatus(index, RemoteDataState.Loading))
|
||||
|
||||
try {
|
||||
const key = getActiveQuery(getState()).builderConfig.tags[index].key
|
||||
const values = await fetcher.findValues(
|
||||
index,
|
||||
queryURL,
|
||||
buckets[0],
|
||||
tagPredicates,
|
||||
key
|
||||
)
|
||||
|
||||
const {values: selectedValues} = tags[index]
|
||||
|
||||
for (const selectedValue of selectedValues) {
|
||||
// Even if the selected values didn't come back in the results, let them
|
||||
// be selected anyway
|
||||
if (!values.includes(selectedValue)) {
|
||||
values.unshift(selectedValue)
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(setBuilderTagValues(index, values))
|
||||
dispatch(loadTagSelector(index + 1))
|
||||
} catch (e) {
|
||||
if (e instanceof CancellationError) {
|
||||
return
|
||||
}
|
||||
|
||||
console.error(e)
|
||||
dispatch(setBuilderTagValuesStatus(index, RemoteDataState.Error))
|
||||
}
|
||||
}
|
||||
|
||||
export const selectTagValue = (index: number, value: string) => async (
|
||||
dispatch: Dispatch<Action>,
|
||||
getState: GetState
|
||||
) => {
|
||||
const tags = getActiveQuery(getState()).builderConfig.tags
|
||||
const values = tags[index].values
|
||||
|
||||
let newValues: string[]
|
||||
|
||||
if (values.includes(value)) {
|
||||
newValues = values.filter(v => v !== value)
|
||||
} else {
|
||||
newValues = [...values, value]
|
||||
}
|
||||
|
||||
dispatch(setBuilderTagValuesSelection(index, newValues))
|
||||
|
||||
if (index === tags.length - 1 && newValues.length) {
|
||||
dispatch(addTagSelector())
|
||||
} else {
|
||||
dispatch(loadTagSelector(index + 1))
|
||||
}
|
||||
}
|
||||
|
||||
export const selectTagKey = (index: number, key: string) => async (
|
||||
dispatch: Dispatch<Action>
|
||||
) => {
|
||||
dispatch(setBuilderTagKeySelection(index, key))
|
||||
dispatch(loadTagSelectorValues(index))
|
||||
}
|
||||
|
||||
// TODO
|
||||
export const searchTagValues = (/* index: number, searchTerm: string */) => async () => {}
|
||||
|
||||
export const addTagSelector = () => async (
|
||||
dispatch: Dispatch<Action>,
|
||||
getState: GetState
|
||||
) => {
|
||||
dispatch(addTagSelectorSync())
|
||||
|
||||
const newIndex = getActiveQuery(getState()).builderConfig.tags.length - 1
|
||||
|
||||
dispatch(loadTagSelector(newIndex))
|
||||
}
|
||||
|
||||
export const removeTagSelector = (index: number) => async (
|
||||
dispatch: Dispatch<Action>
|
||||
) => {
|
||||
fetcher.cancelFindValues(index)
|
||||
fetcher.cancelFindKeys(index)
|
||||
|
||||
dispatch(removeTagSelectorSync(index))
|
||||
dispatch(loadTagSelector(index))
|
||||
}
|
|
@ -1,6 +1,11 @@
|
|||
// Actions
|
||||
import {loadBuckets} from 'src/shared/actions/v2/queryBuilder'
|
||||
|
||||
// Types
|
||||
import {Dispatch} from 'redux-thunk'
|
||||
import {TimeMachineState} from 'src/shared/reducers/v2/timeMachines'
|
||||
import {TimeRange, ViewType, BuilderConfig} from 'src/types/v2'
|
||||
import {Action as QueryBuilderAction} from 'src/shared/actions/v2/queryBuilder'
|
||||
import {TimeRange, ViewType} from 'src/types/v2'
|
||||
import {
|
||||
Axes,
|
||||
DecimalPlaces,
|
||||
|
@ -12,6 +17,7 @@ import {TimeMachineTab} from 'src/types/v2/timeMachine'
|
|||
import {Color} from 'src/types/colors'
|
||||
|
||||
export type Action =
|
||||
| QueryBuilderAction
|
||||
| SetActiveTimeMachineAction
|
||||
| SetActiveTabAction
|
||||
| SetNameAction
|
||||
|
@ -34,7 +40,6 @@ export type Action =
|
|||
| SetYAxisSuffix
|
||||
| SetYAxisBase
|
||||
| SetYAxisScale
|
||||
| SetQuerySourceAction
|
||||
| SetPrefix
|
||||
| SetSuffix
|
||||
| IncrementSubmitToken
|
||||
|
@ -44,7 +49,6 @@ export type Action =
|
|||
| EditActiveQueryAsFluxAction
|
||||
| EditActiveQueryAsInfluxQLAction
|
||||
| EditActiveQueryWithBuilderAction
|
||||
| BuildQueryAction
|
||||
| UpdateActiveQueryNameAction
|
||||
| SetFieldOptionsAction
|
||||
| SetTableOptionsAction
|
||||
|
@ -296,16 +300,6 @@ export const setTextThresholdColoring = (): SetTextThresholdColoringAction => ({
|
|||
type: 'SET_TEXT_THRESHOLD_COLORING',
|
||||
})
|
||||
|
||||
interface SetQuerySourceAction {
|
||||
type: 'SET_QUERY_SOURCE'
|
||||
payload: {sourceID: string}
|
||||
}
|
||||
|
||||
export const setQuerySource = (sourceID: string): SetQuerySourceAction => ({
|
||||
type: 'SET_QUERY_SOURCE',
|
||||
payload: {sourceID},
|
||||
})
|
||||
|
||||
interface IncrementSubmitToken {
|
||||
type: 'INCREMENT_SUBMIT_TOKEN'
|
||||
}
|
||||
|
@ -343,41 +337,50 @@ interface SetActiveQueryIndexAction {
|
|||
payload: {activeQueryIndex: number}
|
||||
}
|
||||
|
||||
export const setActiveQueryIndex = (
|
||||
export const setActiveQueryIndexSync = (
|
||||
activeQueryIndex: number
|
||||
): SetActiveQueryIndexAction => ({
|
||||
type: 'SET_ACTIVE_QUERY_INDEX',
|
||||
payload: {activeQueryIndex},
|
||||
})
|
||||
|
||||
export const setActiveQueryIndex = (activeQueryIndex: number) => (
|
||||
dispatch: Dispatch<Action>
|
||||
) => {
|
||||
dispatch(setActiveQueryIndexSync(activeQueryIndex))
|
||||
dispatch(loadBuckets())
|
||||
}
|
||||
|
||||
interface AddQueryAction {
|
||||
type: 'ADD_QUERY'
|
||||
}
|
||||
|
||||
export const addQuery = (): AddQueryAction => ({
|
||||
export const addQuerySync = (): AddQueryAction => ({
|
||||
type: 'ADD_QUERY',
|
||||
})
|
||||
|
||||
export const addQuery = () => (dispatch: Dispatch<Action>) => {
|
||||
dispatch(addQuerySync())
|
||||
dispatch(loadBuckets())
|
||||
}
|
||||
|
||||
interface RemoveQueryAction {
|
||||
type: 'REMOVE_QUERY'
|
||||
payload: {queryIndex: number}
|
||||
}
|
||||
|
||||
export const removeQuery = (queryIndex: number): RemoveQueryAction => ({
|
||||
export const removeQuerySync = (queryIndex: number): RemoveQueryAction => ({
|
||||
type: 'REMOVE_QUERY',
|
||||
payload: {queryIndex},
|
||||
})
|
||||
|
||||
interface BuildQueryAction {
|
||||
type: 'BUILD_QUERY'
|
||||
payload: {builderConfig: BuilderConfig}
|
||||
export const removeQuery = (queryIndex: number) => (
|
||||
dispatch: Dispatch<Action>
|
||||
) => {
|
||||
dispatch(removeQuerySync(queryIndex))
|
||||
dispatch(loadBuckets())
|
||||
}
|
||||
|
||||
export const buildQuery = (builderConfig: BuilderConfig): BuildQueryAction => ({
|
||||
type: 'BUILD_QUERY',
|
||||
payload: {builderConfig},
|
||||
})
|
||||
|
||||
interface UpdateActiveQueryNameAction {
|
||||
type: 'UPDATE_ACTIVE_QUERY_NAME'
|
||||
payload: {queryName: string}
|
||||
|
|
|
@ -1,135 +1,81 @@
|
|||
// Libraries
|
||||
import {get} from 'lodash'
|
||||
import uuid from 'uuid'
|
||||
|
||||
// APIs
|
||||
import {executeQuery, ExecuteFluxQueryResult} from 'src/shared/apis/v2/query'
|
||||
import {parseResponse} from 'src/shared/parsing/flux/response'
|
||||
|
||||
// Types
|
||||
import {InfluxLanguage} from 'src/types/v2'
|
||||
import {Source} from 'src/api'
|
||||
// Utils
|
||||
import {formatTagFilterCall} from 'src/shared/utils/queryBuilder'
|
||||
|
||||
export const SEARCH_DURATION = '30d'
|
||||
// Types
|
||||
import {InfluxLanguage, BuilderConfig} from 'src/types/v2'
|
||||
|
||||
export const SEARCH_DURATION = '5m'
|
||||
export const LIMIT = 200
|
||||
|
||||
export async function findBuckets(
|
||||
url: string,
|
||||
sourceType: Source.TypeEnum,
|
||||
searchTerm?: string
|
||||
) {
|
||||
if (sourceType === Source.TypeEnum.V1) {
|
||||
throw new Error('metaqueries not yet implemented for SourceType.V1')
|
||||
}
|
||||
async function findBuckets(url: string): Promise<string[]> {
|
||||
const query = `buckets()
|
||||
|> sort(columns: ["name"])
|
||||
|> limit(n: ${LIMIT})`
|
||||
|
||||
const resp = await findBucketsFlux(url, searchTerm)
|
||||
const resp = await executeQuery(url, query, InfluxLanguage.Flux)
|
||||
const parsed = extractCol(resp, 'name')
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
export async function findMeasurements(
|
||||
async function findKeys(
|
||||
url: string,
|
||||
sourceType: Source.TypeEnum,
|
||||
bucket: string,
|
||||
tagsSelections: BuilderConfig['tags'],
|
||||
searchTerm: string = ''
|
||||
): Promise<string[]> {
|
||||
if (sourceType === Source.TypeEnum.V1) {
|
||||
throw new Error('metaqueries not yet implemented for SourceType.V1')
|
||||
}
|
||||
const tagFilters = formatTagFilterCall(tagsSelections)
|
||||
const searchFilter = formatSearchFilterCall(searchTerm)
|
||||
const previousKeyFilter = formatTagKeyFilterCall(tagsSelections)
|
||||
|
||||
const resp = await findMeasurementsFlux(url, bucket, searchTerm)
|
||||
const parsed = extractCol(resp, '_measurement')
|
||||
const query = `from(bucket: "${bucket}")
|
||||
|> range(start: -${SEARCH_DURATION})${tagFilters}
|
||||
|> keys(except: ["_time", "_start", "_stop", "_value"])
|
||||
|> group()
|
||||
|> distinct()
|
||||
|> keep(columns: ["_value"])${searchFilter}${previousKeyFilter}
|
||||
|> sort()
|
||||
|> limit(n: ${LIMIT})`
|
||||
|
||||
const resp = await executeQuery(url, query, InfluxLanguage.Flux)
|
||||
const parsed = extractCol(resp, '_value')
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
export async function findFields(
|
||||
async function findValues(
|
||||
url: string,
|
||||
sourceType: Source.TypeEnum,
|
||||
bucket: string,
|
||||
measurements: string[],
|
||||
tagsSelections: BuilderConfig['tags'],
|
||||
key: string,
|
||||
searchTerm: string = ''
|
||||
): Promise<string[]> {
|
||||
if (sourceType === Source.TypeEnum.V1) {
|
||||
throw new Error('metaqueries not yet implemented for SourceType.V1')
|
||||
}
|
||||
const tagFilters = formatTagFilterCall(tagsSelections)
|
||||
const searchFilter = formatSearchFilterCall(searchTerm)
|
||||
|
||||
const resp = await findFieldsFlux(url, bucket, measurements, searchTerm)
|
||||
const parsed = extractCol(resp, '_field')
|
||||
const query = `from(bucket: "${bucket}")
|
||||
|> range(start: -${SEARCH_DURATION})${tagFilters}
|
||||
|> group(columns: ["${key}"])
|
||||
|> distinct(column: "${key}")
|
||||
|> group()
|
||||
|> keep(columns: ["_value"])${searchFilter}
|
||||
|> sort()
|
||||
|> limit(n: ${LIMIT})`
|
||||
|
||||
const resp = await executeQuery(url, query, InfluxLanguage.Flux)
|
||||
const parsed = extractCol(resp, '_value')
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
function findBucketsFlux(
|
||||
url: string,
|
||||
searchTerm: string
|
||||
): Promise<ExecuteFluxQueryResult> {
|
||||
let query = 'buckets()'
|
||||
|
||||
if (searchTerm) {
|
||||
query += `
|
||||
|> filter(fn: (r) => r.name =~ /(?i:${searchTerm})/)`
|
||||
}
|
||||
|
||||
query += `
|
||||
|> sort(columns: ["name"])
|
||||
|> limit(n: ${LIMIT})`
|
||||
|
||||
return executeQuery(url, query, InfluxLanguage.Flux)
|
||||
}
|
||||
|
||||
function findMeasurementsFlux(
|
||||
url: string,
|
||||
bucket: string,
|
||||
searchTerm: string
|
||||
): Promise<ExecuteFluxQueryResult> {
|
||||
let query = `from(bucket: "${bucket}")
|
||||
|> range(start: -${SEARCH_DURATION})`
|
||||
|
||||
if (searchTerm) {
|
||||
query += `
|
||||
|> filter(fn: (r) => r._measurement =~ /(?i:${searchTerm})/)`
|
||||
}
|
||||
|
||||
query += `
|
||||
|> group(columns: ["_measurement"])
|
||||
|> distinct(column: "_measurement")
|
||||
|> group()
|
||||
|> sort(columns: ["_measurement"])
|
||||
|> limit(n: ${LIMIT})`
|
||||
|
||||
return executeQuery(url, query, InfluxLanguage.Flux)
|
||||
}
|
||||
|
||||
function findFieldsFlux(
|
||||
url: string,
|
||||
bucket: string,
|
||||
measurements: string[],
|
||||
searchTerm: string
|
||||
): Promise<ExecuteFluxQueryResult> {
|
||||
const measurementPredicate = measurements
|
||||
.map(m => `r._measurement == "${m}"`)
|
||||
.join(' or ')
|
||||
|
||||
let query = `from(bucket: "${bucket}")
|
||||
|> range(start: -${SEARCH_DURATION})
|
||||
|> filter(fn: (r) => ${measurementPredicate})`
|
||||
|
||||
if (searchTerm) {
|
||||
query += `
|
||||
|> filter(fn: (r) => r._field =~ /(?i:${searchTerm})/)`
|
||||
}
|
||||
|
||||
query += `
|
||||
|> group(columns: ["_field"])
|
||||
|> distinct(column: "_field")
|
||||
|> group()
|
||||
|> sort(columns: ["_field"])
|
||||
|> limit(n: ${LIMIT})`
|
||||
|
||||
return executeQuery(url, query, InfluxLanguage.Flux)
|
||||
}
|
||||
|
||||
function extractCol(resp: ExecuteFluxQueryResult, colName: string): string[] {
|
||||
const tables = parseResponse(resp.csv)
|
||||
const data = get(tables, '0.data', [])
|
||||
|
@ -152,3 +98,100 @@ function extractCol(resp: ExecuteFluxQueryResult, colName: string): string[] {
|
|||
|
||||
return colValues
|
||||
}
|
||||
|
||||
function formatTagKeyFilterCall(tagsSelections: BuilderConfig['tags']) {
|
||||
const keys = tagsSelections.map(({key}) => key)
|
||||
|
||||
if (!keys.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const fnBody = keys.map(key => `r._value != "${key}"`).join(' and ')
|
||||
|
||||
return `\n |> filter(fn: (r) => ${fnBody})`
|
||||
}
|
||||
|
||||
function formatSearchFilterCall(searchTerm: string) {
|
||||
if (!searchTerm) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return `\n |> filter(fn: (r) => r._value =~ /(?i:${searchTerm})/)`
|
||||
}
|
||||
|
||||
export class CancellationError extends Error {}
|
||||
|
||||
export class QueryBuilderFetcher {
|
||||
private findBucketsToken: string = ''
|
||||
private findKeysTokens = []
|
||||
private findValuesTokens = []
|
||||
|
||||
public async findBuckets(url: string): Promise<string[]> {
|
||||
const token = uuid.v4()
|
||||
|
||||
this.findBucketsToken = token
|
||||
|
||||
const result = await findBuckets(url)
|
||||
|
||||
if (token !== this.findBucketsToken) {
|
||||
throw new CancellationError()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public async findKeys(
|
||||
index: number,
|
||||
url: string,
|
||||
bucket: string,
|
||||
tagsSelections: BuilderConfig['tags'],
|
||||
searchTerm: string = ''
|
||||
): Promise<string[]> {
|
||||
const token = uuid.v4()
|
||||
|
||||
this.findKeysTokens[index] = token
|
||||
|
||||
const result = await findKeys(url, bucket, tagsSelections, searchTerm)
|
||||
|
||||
if (token !== this.findKeysTokens[index]) {
|
||||
throw new CancellationError()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public cancelFindKeys(index) {
|
||||
this.findKeysTokens[index] = uuid.v4()
|
||||
}
|
||||
|
||||
public async findValues(
|
||||
index: number,
|
||||
url: string,
|
||||
bucket: string,
|
||||
tagsSelections: BuilderConfig['tags'],
|
||||
key: string,
|
||||
searchTerm: string = ''
|
||||
): Promise<string[]> {
|
||||
const token = uuid.v4()
|
||||
|
||||
this.findValuesTokens[index] = token
|
||||
|
||||
const result = await findValues(
|
||||
url,
|
||||
bucket,
|
||||
tagsSelections,
|
||||
key,
|
||||
searchTerm
|
||||
)
|
||||
|
||||
if (token !== this.findValuesTokens[index]) {
|
||||
throw new CancellationError()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public cancelFindValues(index) {
|
||||
this.findValuesTokens[index] = uuid.v4()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
@import "src/style/modules";
|
||||
|
||||
.builder-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 150px;
|
||||
background: $g4-onyx;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.builder-card-search-bar {
|
||||
margin: 10px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.builder-card--items {
|
||||
flex: 1 1 0;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.builder-card--item {
|
||||
font-weight: 500;
|
||||
padding: 5px 15px;
|
||||
margin-bottom: 1px;
|
||||
cursor: pointer;
|
||||
color: $g14-chromium;
|
||||
font-family: $code-font;
|
||||
|
||||
&.selected {
|
||||
background: $c-pool;
|
||||
color: $g18-cloud;
|
||||
}
|
||||
|
||||
&.selected:hover {
|
||||
background: $c-pool;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $g4-onyx;
|
||||
}
|
||||
}
|
||||
|
||||
.builder-card--empty {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
color: $g8-storm;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
|
||||
.waiting-text {
|
||||
text-align: start;
|
||||
width: 85px;
|
||||
margin: 0 auto;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.builder-card-limit-message {
|
||||
padding: 15px;
|
||||
color: $g9-mountain;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
|
||||
// Components
|
||||
import BuilderCardSearchBar from 'src/shared/components/BuilderCardSearchBar'
|
||||
import BuilderCardLimitMessage from 'src/shared/components/BuilderCardLimitMessage'
|
||||
import WaitingText from 'src/shared/components/WaitingText'
|
||||
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
|
||||
|
||||
// Styles
|
||||
import 'src/shared/components/BuilderCard.scss'
|
||||
|
||||
// Types
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
items: string[]
|
||||
selectedItems: string[]
|
||||
onSelectItems: (selection: string[]) => void
|
||||
onSearch: (searchTerm: string) => Promise<void>
|
||||
status?: RemoteDataState
|
||||
emptyText?: string
|
||||
limitCount?: number
|
||||
singleSelect?: boolean
|
||||
}
|
||||
|
||||
class BuilderCard extends PureComponent<Props> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
status: RemoteDataState.Done,
|
||||
emptyText: 'None Found',
|
||||
limitCount: Infinity,
|
||||
singleSelect: false,
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <div className="builder-card">{this.body}</div>
|
||||
}
|
||||
|
||||
private get body(): JSX.Element {
|
||||
const {status, onSearch, emptyText} = this.props
|
||||
|
||||
if (status === RemoteDataState.Error) {
|
||||
return <div className="builder-card--empty">Failed to load</div>
|
||||
}
|
||||
|
||||
if (status === RemoteDataState.NotStarted) {
|
||||
return <div className="builder-card--empty">{emptyText}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BuilderCardSearchBar onSearch={onSearch} />
|
||||
{this.items}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private get items(): JSX.Element {
|
||||
const {status, items, limitCount} = this.props
|
||||
|
||||
if (status === RemoteDataState.Loading) {
|
||||
return (
|
||||
<div className="builder-card--empty">
|
||||
<WaitingText text="Loading" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="builder-card--items">
|
||||
{items.length ? (
|
||||
<FancyScrollbar>
|
||||
{items.map(item => (
|
||||
<div
|
||||
key={item}
|
||||
className={this.itemClass(item)}
|
||||
onClick={this.handleToggleItem(item)}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</FancyScrollbar>
|
||||
) : (
|
||||
<div className="builder-card--empty">Nothing found</div>
|
||||
)}
|
||||
</div>
|
||||
<BuilderCardLimitMessage
|
||||
itemCount={items.length}
|
||||
limitCount={limitCount}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private itemClass = (item): string => {
|
||||
if (this.props.selectedItems.includes(item)) {
|
||||
return 'builder-card--item selected'
|
||||
}
|
||||
|
||||
return 'builder-card--item'
|
||||
}
|
||||
|
||||
private handleToggleItem = (item: string) => (): void => {
|
||||
const {singleSelect, selectedItems, onSelectItems} = this.props
|
||||
|
||||
if (singleSelect && selectedItems[0] === item) {
|
||||
onSelectItems([])
|
||||
} else if (singleSelect) {
|
||||
onSelectItems([item])
|
||||
} else if (selectedItems.includes(item)) {
|
||||
onSelectItems(selectedItems.filter(x => x !== item))
|
||||
} else {
|
||||
onSelectItems([...selectedItems, item])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BuilderCard
|
|
@ -1,20 +0,0 @@
|
|||
import React, {SFC} from 'react'
|
||||
|
||||
interface Props {
|
||||
itemCount: number
|
||||
limitCount: number
|
||||
}
|
||||
|
||||
const BuilderCardLimitMessage: SFC<Props> = ({itemCount, limitCount}) => {
|
||||
if (itemCount < limitCount) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="builder-card-limit-message">
|
||||
Showing first {limitCount} results. Use the search bar to find more.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BuilderCardLimitMessage
|
|
@ -1,63 +0,0 @@
|
|||
// Libraries
|
||||
import React, {PureComponent, ChangeEvent} from 'react'
|
||||
|
||||
// Components
|
||||
import {Input, ComponentSize} from 'src/clockface'
|
||||
|
||||
// Utils
|
||||
import DefaultDebouncer, {Debouncer} from 'src/shared/utils/debouncer'
|
||||
import {CancellationError} from 'src/utils/restartable'
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 350
|
||||
|
||||
interface Props {
|
||||
onSearch: (searchTerm: string) => Promise<void>
|
||||
}
|
||||
|
||||
interface State {
|
||||
searchTerm: string
|
||||
}
|
||||
|
||||
class BuilderCardSearchBar extends PureComponent<Props, State> {
|
||||
public state: State = {searchTerm: ''}
|
||||
|
||||
private debouncer: Debouncer = new DefaultDebouncer()
|
||||
|
||||
public render() {
|
||||
const {searchTerm} = this.state
|
||||
|
||||
return (
|
||||
<div className="builder-card-search-bar">
|
||||
<Input
|
||||
size={ComponentSize.Small}
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({searchTerm: e.target.value}, () =>
|
||||
this.debouncer.call(this.emitChange, SEARCH_DEBOUNCE_MS)
|
||||
)
|
||||
}
|
||||
|
||||
private emitChange = async () => {
|
||||
const {onSearch} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
||||
try {
|
||||
await onSearch(searchTerm)
|
||||
} catch (e) {
|
||||
if (e instanceof CancellationError) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({searchTerm: ''})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BuilderCardSearchBar
|
|
@ -0,0 +1,26 @@
|
|||
@import "src/style/modules";
|
||||
|
||||
.function-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px;
|
||||
background: $g4-onyx;
|
||||
|
||||
h3 {
|
||||
margin: 10px 5px 10px 10px;
|
||||
padding: 5px 0 0 0;
|
||||
text-transform: uppercase;
|
||||
color: $g10-wolf;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.function-selector--search {
|
||||
margin: 0 10px 10px 10px;
|
||||
width: initial !important;
|
||||
}
|
||||
|
||||
.function-selector .selector-list {
|
||||
flex: 1 1 0;
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
// Libraries
|
||||
import React, {PureComponent, ChangeEvent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
// Components
|
||||
import {Input} from 'src/clockface'
|
||||
import SelectorList from 'src/shared/components/SelectorList'
|
||||
|
||||
// Actions
|
||||
import {selectFunction} from 'src/shared/actions/v2/queryBuilder'
|
||||
|
||||
// Utils
|
||||
import {getActiveQuery} from 'src/shared/selectors/timeMachines'
|
||||
|
||||
// Constants
|
||||
import {FUNCTIONS} from 'src/shared/constants/queryBuilder'
|
||||
|
||||
// Styles
|
||||
import 'src/shared/components/FunctionSelector.scss'
|
||||
|
||||
// Types
|
||||
import {AppState, BuilderConfig} from 'src/types/v2'
|
||||
|
||||
const FUNCTION_NAMES = FUNCTIONS.map(f => f.name)
|
||||
|
||||
interface StateProps {
|
||||
selectedFunctions: BuilderConfig['functions']
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
onSelectFunction: (fnName: string) => void
|
||||
}
|
||||
|
||||
type Props = StateProps & DispatchProps
|
||||
|
||||
interface State {
|
||||
searchTerm: string
|
||||
}
|
||||
|
||||
class FunctionSelector extends PureComponent<Props, State> {
|
||||
public state: State = {searchTerm: ''}
|
||||
|
||||
public render() {
|
||||
const {onSelectFunction} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
||||
return (
|
||||
<div className="function-selector">
|
||||
<h3>Aggregate Functions</h3>
|
||||
<Input
|
||||
customClass={'function-selector--search'}
|
||||
value={searchTerm}
|
||||
onChange={this.handleSetSearchTerm}
|
||||
placeholder="Search functions..."
|
||||
/>
|
||||
<SelectorList
|
||||
items={this.functions}
|
||||
selectedItems={this.selectedFunctions}
|
||||
onSelectItem={onSelectFunction}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get functions(): string[] {
|
||||
return FUNCTION_NAMES.filter(f => f.includes(this.state.searchTerm))
|
||||
}
|
||||
|
||||
private get selectedFunctions(): string[] {
|
||||
return this.props.selectedFunctions.map(f => f.name)
|
||||
}
|
||||
|
||||
private handleSetSearchTerm = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({searchTerm: e.target.value})
|
||||
}
|
||||
}
|
||||
|
||||
const mstp = (state: AppState) => {
|
||||
const selectedFunctions = getActiveQuery(state).builderConfig.functions
|
||||
|
||||
return {selectedFunctions}
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
onSelectFunction: selectFunction,
|
||||
}
|
||||
|
||||
export default connect<StateProps, DispatchProps>(
|
||||
mstp,
|
||||
mdtp
|
||||
)(FunctionSelector)
|
|
@ -0,0 +1,70 @@
|
|||
// Libraries
|
||||
import React, {SFC} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
// Components
|
||||
import {Dropdown, ComponentSize} from 'src/clockface'
|
||||
|
||||
// Actions
|
||||
import {selectBucket} from 'src/shared/actions/v2/queryBuilder'
|
||||
|
||||
// Utils
|
||||
import {
|
||||
getActiveTimeMachine,
|
||||
getActiveQuery,
|
||||
} from 'src/shared/selectors/timeMachines'
|
||||
import {toComponentStatus} from 'src/shared/utils/toComponentStatus'
|
||||
|
||||
// Types
|
||||
import {AppState} from 'src/types/v2'
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
||||
interface StateProps {
|
||||
selectedBucket: string
|
||||
buckets: string[]
|
||||
bucketsStatus: RemoteDataState
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
onSelectBucket: (bucket: string) => void
|
||||
}
|
||||
|
||||
interface OwnProps {}
|
||||
|
||||
type Props = StateProps & DispatchProps & OwnProps
|
||||
|
||||
const QueryBuilderBucketDropdown: SFC<Props> = props => {
|
||||
const {selectedBucket, buckets, bucketsStatus, onSelectBucket} = props
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
selectedID={selectedBucket}
|
||||
onChange={onSelectBucket}
|
||||
buttonSize={ComponentSize.Small}
|
||||
status={toComponentStatus(bucketsStatus)}
|
||||
>
|
||||
{buckets.map(bucket => (
|
||||
<Dropdown.Item key={bucket} id={bucket} value={bucket}>
|
||||
{bucket}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
const mstp = (state: AppState) => {
|
||||
const {buckets, bucketsStatus} = getActiveTimeMachine(state).queryBuilder
|
||||
const selectedBucket =
|
||||
getActiveQuery(state).builderConfig.buckets[0] || buckets[0]
|
||||
|
||||
return {selectedBucket, buckets, bucketsStatus}
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
onSelectBucket: selectBucket as any,
|
||||
}
|
||||
|
||||
export default connect<StateProps, DispatchProps, OwnProps>(
|
||||
mstp,
|
||||
mdtp
|
||||
)(QueryBuilderBucketDropdown)
|
|
@ -0,0 +1,25 @@
|
|||
@import "src/style/modules";
|
||||
|
||||
.selector-list--item {
|
||||
font-weight: 500;
|
||||
padding: 5px 15px;
|
||||
margin-bottom: 1px;
|
||||
cursor: pointer;
|
||||
color: $g14-chromium;
|
||||
font-family: $code-font;
|
||||
font-size: 13px;
|
||||
|
||||
&.selected {
|
||||
background: $c-pool;
|
||||
color: $g18-cloud;
|
||||
}
|
||||
|
||||
&.selected:hover {
|
||||
background: $c-pool;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $g5-pepper;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import React, {SFC} from 'react'
|
||||
|
||||
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
|
||||
|
||||
import 'src/shared/components/SelectorList.scss'
|
||||
|
||||
interface Props {
|
||||
items: string[]
|
||||
selectedItems: string[]
|
||||
onSelectItem: (item: string) => void
|
||||
}
|
||||
|
||||
const SelectorList: SFC<Props> = props => {
|
||||
const {items, selectedItems, onSelectItem} = props
|
||||
|
||||
return (
|
||||
<div className="selector-list">
|
||||
<FancyScrollbar>
|
||||
{items.map(item => {
|
||||
const selectedClass = selectedItems.includes(item) ? 'selected' : ''
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`selector-list--item ${selectedClass}`}
|
||||
key={item}
|
||||
onClick={() => onSelectItem(item)}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectorList
|
|
@ -0,0 +1,53 @@
|
|||
@import "src/style/modules";
|
||||
|
||||
.tag-selector {
|
||||
min-height: 130px;
|
||||
background: $g4-onyx;
|
||||
border-radius: 4px;
|
||||
padding: 10px 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 13px;
|
||||
|
||||
input {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-selector--top, .tag-selector--search {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.tag-selector--top {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tag-selector--search {
|
||||
margin-bottom: 10px;
|
||||
width: initial !important;
|
||||
}
|
||||
|
||||
button.tag-selector--remove {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.tag-selector .selector-list {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
|
||||
.tag-selector--empty {
|
||||
flex: 1 1 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: $g8-storm;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
// Libraries
|
||||
import React, {PureComponent, ChangeEvent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
// Components
|
||||
import {Dropdown, Input, Button, ButtonShape, IconFont} from 'src/clockface'
|
||||
import WaitingText from 'src/shared/components/WaitingText'
|
||||
import SelectorList from 'src/shared/components/SelectorList'
|
||||
|
||||
// Actions
|
||||
import {
|
||||
selectTagKey,
|
||||
selectTagValue,
|
||||
searchTagValues,
|
||||
removeTagSelector,
|
||||
} from 'src/shared/actions/v2/queryBuilder'
|
||||
|
||||
// Utils
|
||||
import {toComponentStatus} from 'src/shared/utils/toComponentStatus'
|
||||
import DefaultDebouncer from 'src/shared/utils/debouncer'
|
||||
import {
|
||||
getActiveQuery,
|
||||
getActiveTimeMachine,
|
||||
} from 'src/shared/selectors/timeMachines'
|
||||
|
||||
// Styles
|
||||
import 'src/shared/components/TagSelector.scss'
|
||||
|
||||
// Types
|
||||
import {AppState} from 'src/types/v2'
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 500
|
||||
|
||||
interface StateProps {
|
||||
emptyText: string
|
||||
keys: string[]
|
||||
keysStatus: RemoteDataState
|
||||
selectedKey: string
|
||||
values: string[]
|
||||
valuesStatus: RemoteDataState
|
||||
selectedValues: string[]
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
onSelectValue: (index: number, value: string) => void
|
||||
onSelectTag: (index: number, tag: string) => void
|
||||
onSearchValues: (index: number, searchTerm: string) => void
|
||||
onRemoveTagSelector: (index: number) => void
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
index: number
|
||||
}
|
||||
|
||||
type Props = StateProps & DispatchProps & OwnProps
|
||||
|
||||
interface State {
|
||||
searchTerm: string
|
||||
}
|
||||
|
||||
class TagSelector extends PureComponent<Props, State> {
|
||||
public state: State = {searchTerm: ''}
|
||||
|
||||
private debouncer = new DefaultDebouncer()
|
||||
|
||||
public render() {
|
||||
return <div className="tag-selector">{this.body}</div>
|
||||
}
|
||||
|
||||
private get body() {
|
||||
const {index, keys, keysStatus, selectedKey, emptyText} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
||||
if (keysStatus === RemoteDataState.NotStarted) {
|
||||
return <div className="tag-selector--empty">{emptyText}</div>
|
||||
}
|
||||
|
||||
if (keysStatus === RemoteDataState.Loading) {
|
||||
return (
|
||||
<div className="tag-selector--empty">
|
||||
<WaitingText text="Loading tag keys" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (keysStatus === RemoteDataState.Error) {
|
||||
return <div className="tag-selector--empty">Failed to load tag keys</div>
|
||||
}
|
||||
|
||||
if (keysStatus === RemoteDataState.Done && !keys.length) {
|
||||
return <div className="tag-selector--empty">No more tag keys found</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="tag-selector--top">
|
||||
<Dropdown
|
||||
selectedID={selectedKey}
|
||||
onChange={this.handleSelectTag}
|
||||
status={toComponentStatus(keysStatus)}
|
||||
titleText="No Tags Found"
|
||||
>
|
||||
{keys.map(key => (
|
||||
<Dropdown.Item key={key} id={key} value={key}>
|
||||
{key}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
{index !== 0 && (
|
||||
<Button
|
||||
shape={ButtonShape.Square}
|
||||
icon={IconFont.Remove}
|
||||
onClick={this.handleRemoveTagSelector}
|
||||
customClass="tag-selector--remove"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
value={searchTerm}
|
||||
placeholder={`Search ${selectedKey} tag values`}
|
||||
customClass="tag-selector--search"
|
||||
onChange={this.handleSearch}
|
||||
/>
|
||||
{this.values}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private get values() {
|
||||
const {selectedKey, values, valuesStatus, selectedValues} = this.props
|
||||
|
||||
if (valuesStatus === RemoteDataState.Error) {
|
||||
return (
|
||||
<div className="tag-selector--empty">
|
||||
{`Failed to load tag values for ${selectedKey}`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (valuesStatus === RemoteDataState.Loading) {
|
||||
return (
|
||||
<div className="tag-selector--empty">
|
||||
<WaitingText text="Loading tag values" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (valuesStatus === RemoteDataState.Done && !values.length) {
|
||||
return <div className="tag-selector--empty">Nothing found</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectorList
|
||||
items={values}
|
||||
selectedItems={selectedValues}
|
||||
onSelectItem={this.handleSelectValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private handleSelectTag = (tag: string): void => {
|
||||
const {index, onSelectTag} = this.props
|
||||
|
||||
onSelectTag(index, tag)
|
||||
}
|
||||
|
||||
private handleSelectValue = (value: string): void => {
|
||||
const {index, onSelectValue} = this.props
|
||||
|
||||
onSelectValue(index, value)
|
||||
}
|
||||
|
||||
private handleRemoveTagSelector = () => {
|
||||
const {index, onRemoveTagSelector} = this.props
|
||||
|
||||
onRemoveTagSelector(index)
|
||||
}
|
||||
|
||||
private handleSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const {value} = e.target
|
||||
|
||||
this.setState({searchTerm: value}, () => {
|
||||
this.debouncer.call(this.emitSearch, SEARCH_DEBOUNCE_MS)
|
||||
})
|
||||
}
|
||||
|
||||
private emitSearch = () => {
|
||||
const {index, onSearchValues} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
||||
onSearchValues(index, searchTerm)
|
||||
}
|
||||
}
|
||||
|
||||
const mstp = (state: AppState, ownProps: OwnProps): StateProps => {
|
||||
const {keys, keysStatus, values, valuesStatus} = getActiveTimeMachine(
|
||||
state
|
||||
).queryBuilder.tags[ownProps.index]
|
||||
|
||||
const tags = getActiveQuery(state).builderConfig.tags
|
||||
const {key: selectedKey, values: selectedValues} = tags[ownProps.index]
|
||||
|
||||
let emptyText: string
|
||||
|
||||
if (ownProps.index === 0 || !tags[ownProps.index - 1].key) {
|
||||
emptyText = ''
|
||||
} else {
|
||||
emptyText = `Select a ${tags[ownProps.index - 1].key} value first`
|
||||
}
|
||||
|
||||
return {
|
||||
emptyText,
|
||||
keys,
|
||||
keysStatus,
|
||||
selectedKey,
|
||||
values,
|
||||
valuesStatus,
|
||||
selectedValues,
|
||||
}
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
onSelectValue: selectTagValue,
|
||||
onSelectTag: selectTagKey,
|
||||
onSearchValues: searchTagValues,
|
||||
onRemoveTagSelector: removeTagSelector,
|
||||
}
|
||||
|
||||
export default connect<StateProps, DispatchProps, OwnProps>(
|
||||
mstp,
|
||||
mdtp
|
||||
)(TagSelector)
|
|
@ -11,7 +11,7 @@ import TimeMachineVis from 'src/shared/components/TimeMachineVis'
|
|||
import TimeSeries from 'src/shared/components/TimeSeries'
|
||||
|
||||
// Constants
|
||||
const INITIAL_RESIZER_HANDLE = 0.6
|
||||
const INITIAL_RESIZER_HANDLE = 0.5
|
||||
|
||||
// Utils
|
||||
import {getActiveTimeMachine} from 'src/shared/selectors/timeMachines'
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
ComponentSpacer,
|
||||
Alignment,
|
||||
} from 'src/clockface'
|
||||
import TimeMachineSourceDropdown from 'src/shared/components/TimeMachineSourceDropdown'
|
||||
import TimeMachineRefreshDropdown from 'src/shared/components/TimeMachineRefreshDropdown'
|
||||
import ViewTypeDropdown from 'src/shared/components/view_options/ViewTypeDropdown'
|
||||
|
||||
|
@ -56,7 +55,6 @@ class TimeMachineControls extends PureComponent<Props> {
|
|||
return (
|
||||
<div className="time-machine--controls">
|
||||
<ComponentSpacer align={Alignment.Left}>
|
||||
<TimeMachineSourceDropdown />
|
||||
<ViewTypeDropdown />
|
||||
</ComponentSpacer>
|
||||
<ComponentSpacer align={Alignment.Right}>
|
||||
|
|
|
@ -88,7 +88,9 @@ const TimeMachineQueries: SFC<Props> = props => {
|
|||
</div>
|
||||
<div className="time-machine-queries--buttons">
|
||||
<TimeMachineQueriesSwitcher />
|
||||
<SubmitQueryButton queryStatus={queryStatus} />
|
||||
{activeQuery.editMode !== QueryEditMode.Builder && (
|
||||
<SubmitQueryButton queryStatus={queryStatus} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="time-machine-queries--body">{queryEditor}</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, {PureComponent} from 'react'
|
|||
import {connect} from 'react-redux'
|
||||
|
||||
// Components
|
||||
import {Button, Dropdown} from 'src/clockface'
|
||||
import {Button} from 'src/clockface'
|
||||
|
||||
// Actions
|
||||
import {
|
||||
|
@ -13,14 +13,18 @@ import {
|
|||
} from 'src/shared/actions/v2/timeMachines'
|
||||
|
||||
// Utils
|
||||
import {getActiveQuery} from 'src/shared/selectors/timeMachines'
|
||||
import {
|
||||
getActiveQuery,
|
||||
getActiveQuerySource,
|
||||
} from 'src/shared/selectors/timeMachines'
|
||||
import {CONFIRM_LEAVE_ADVANCED_MODE} from 'src/shared/copy/v2'
|
||||
|
||||
// Types
|
||||
import {AppState, QueryEditMode} from 'src/types/v2'
|
||||
import {AppState, QueryEditMode, Source} from 'src/types/v2'
|
||||
|
||||
interface StateProps {
|
||||
editMode: QueryEditMode
|
||||
sourceType: Source.TypeEnum
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
|
@ -33,7 +37,7 @@ type Props = StateProps & DispatchProps
|
|||
|
||||
class TimeMachineQueriesSwitcher extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {editMode, onEditAsFlux, onEditAsInfluxQL} = this.props
|
||||
const {editMode, sourceType, onEditAsFlux, onEditAsInfluxQL} = this.props
|
||||
|
||||
if (editMode !== QueryEditMode.Builder) {
|
||||
return (
|
||||
|
@ -44,21 +48,11 @@ class TimeMachineQueriesSwitcher extends PureComponent<Props> {
|
|||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
selectedID=""
|
||||
titleText="Edit Query As..."
|
||||
widthPixels={130}
|
||||
onChange={this.handleChooseLanguage}
|
||||
>
|
||||
<Dropdown.Item id={'influxQL'} value={onEditAsFlux}>
|
||||
Flux
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item id={'flux'} value={onEditAsInfluxQL}>
|
||||
InfluxQL
|
||||
</Dropdown.Item>
|
||||
</Dropdown>
|
||||
)
|
||||
if (sourceType === Source.TypeEnum.V1) {
|
||||
return <Button text="Edit Query As InfluxQL" onClick={onEditAsInfluxQL} />
|
||||
}
|
||||
|
||||
return <Button text="Edit Query As Flux" onClick={onEditAsFlux} />
|
||||
}
|
||||
|
||||
private handleEditWithBuilder = (): void => {
|
||||
|
@ -68,16 +62,13 @@ class TimeMachineQueriesSwitcher extends PureComponent<Props> {
|
|||
onEditWithBuilder()
|
||||
}
|
||||
}
|
||||
|
||||
private handleChooseLanguage = (actionCreator): void => {
|
||||
actionCreator()
|
||||
}
|
||||
}
|
||||
|
||||
const mstp = (state: AppState) => {
|
||||
const editMode = getActiveQuery(state).editMode
|
||||
const sourceType = getActiveQuerySource(state).type
|
||||
|
||||
return {editMode}
|
||||
return {editMode, sourceType}
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
|
|
|
@ -6,46 +6,55 @@
|
|||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include no-user-select();
|
||||
background: $g3-castle;
|
||||
}
|
||||
|
||||
.query-builder--panel {
|
||||
margin-left: 5px;
|
||||
height: 100%;
|
||||
flex: 1 1 0;
|
||||
.query-builder--buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
> .form--element {
|
||||
width: 200px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.query-builder--panel-header {
|
||||
padding: 10px 0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: $g8-storm;
|
||||
.query-builder--cards {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
small {
|
||||
margin-left: 10px;
|
||||
padding: 2px 4px 2px 3px;
|
||||
border-radius: 2px;
|
||||
background-color: $g4-onyx;
|
||||
font-style: italic;
|
||||
font-size: 11px;
|
||||
.query-builder--tag-selectors {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex: 1 1 0;
|
||||
height: 100%;
|
||||
|
||||
.tag-selector {
|
||||
margin-right: 5px;
|
||||
flex: 0 0 250px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.builder-card {
|
||||
height: 100%;
|
||||
flex: 1 1 0;
|
||||
.query-builder .function-selector {
|
||||
flex: 0 0 250px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
button.query-builder--add-tag-selector {
|
||||
height: 100%;
|
||||
background-color: $g4-onyx;
|
||||
border-color: $g4-onyx;
|
||||
margin-right: 5px;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
|
@ -1,375 +1,85 @@
|
|||
// Libraries
|
||||
import React, {PureComponent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {range} from 'lodash'
|
||||
|
||||
// Components
|
||||
import BuilderCard from 'src/shared/components/BuilderCard'
|
||||
|
||||
// APIs
|
||||
import {
|
||||
findBuckets,
|
||||
findMeasurements,
|
||||
findFields,
|
||||
LIMIT,
|
||||
} from 'src/shared/apis/v2/queryBuilder'
|
||||
import {Form, Button, ButtonShape, IconFont} from 'src/clockface'
|
||||
import TagSelector from 'src/shared/components/TagSelector'
|
||||
import QueryBuilderBucketDropdown from 'src/shared/components/QueryBuilderBucketDropdown'
|
||||
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
|
||||
import FunctionSelector from 'src/shared/components/FunctionSelector'
|
||||
|
||||
// Actions
|
||||
import {buildQuery} from 'src/shared/actions/v2/timeMachines'
|
||||
import {loadBuckets, addTagSelector} from 'src/shared/actions/v2/queryBuilder'
|
||||
|
||||
// Utils
|
||||
import {restartable, CancellationError} from 'src/utils/restartable'
|
||||
import {getActiveQuery} from 'src/shared/selectors/timeMachines'
|
||||
import {getSources, getActiveSource} from 'src/sources/selectors'
|
||||
|
||||
// Constants
|
||||
import {FUNCTIONS} from 'src/shared/constants/queryBuilder'
|
||||
|
||||
// Styles
|
||||
import 'src/shared/components/TimeMachineQueryBuilder.scss'
|
||||
|
||||
// Types
|
||||
import {RemoteDataState} from 'src/types'
|
||||
import {AppState, BuilderConfig} from 'src/types/v2'
|
||||
import {Source} from 'src/api'
|
||||
|
||||
const EMPTY_FIELDS_MESSAGE = 'Select at least one bucket and measurement'
|
||||
const EMPTY_FUNCTIONS_MESSAGE = 'Select at least one bucket and measurement'
|
||||
const EMPTY_MEASUREMENTS_MESSAGE = 'Select a bucket'
|
||||
const mergeUnique = (items: string[], selection: string[]) =>
|
||||
[...new Set([...items, ...selection])].sort()
|
||||
import {AppState} from 'src/types/v2'
|
||||
|
||||
interface StateProps {
|
||||
queryURL: string
|
||||
sourceType: Source.TypeEnum
|
||||
tagFiltersLength: number
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
onBuildQuery: typeof buildQuery
|
||||
onLoadBuckets: () => Promise<void>
|
||||
onAddTagSelector: () => void
|
||||
}
|
||||
|
||||
type Props = StateProps & DispatchProps
|
||||
|
||||
interface State {
|
||||
buckets: string[]
|
||||
bucketsStatus: RemoteDataState
|
||||
bucketsSelection: string[]
|
||||
measurements: string[]
|
||||
measurementsStatus: RemoteDataState
|
||||
measurementsSelection: string[]
|
||||
fields: string[]
|
||||
fieldsStatus: RemoteDataState
|
||||
fieldsSelection: string[]
|
||||
functions: string[]
|
||||
functionsStatus: RemoteDataState
|
||||
functionsSelection: string[]
|
||||
}
|
||||
interface State {}
|
||||
|
||||
class TimeMachineQueryBuilder extends PureComponent<Props, State> {
|
||||
public state: State = {
|
||||
buckets: [],
|
||||
bucketsStatus: RemoteDataState.Loading,
|
||||
bucketsSelection: [],
|
||||
measurements: [],
|
||||
measurementsStatus: RemoteDataState.NotStarted,
|
||||
measurementsSelection: [],
|
||||
fields: [],
|
||||
fieldsStatus: RemoteDataState.NotStarted,
|
||||
fieldsSelection: [],
|
||||
functions: FUNCTIONS.map(f => f.name),
|
||||
functionsStatus: RemoteDataState.NotStarted,
|
||||
functionsSelection: [],
|
||||
}
|
||||
|
||||
private findBuckets = restartable(findBuckets)
|
||||
private findMeasurements = restartable(findMeasurements)
|
||||
private findFields = restartable(findFields)
|
||||
|
||||
public componentDidMount() {
|
||||
this.findAndSetBuckets()
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.queryURL !== this.props.queryURL) {
|
||||
this.findAndSetBuckets()
|
||||
}
|
||||
this.props.onLoadBuckets()
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
buckets,
|
||||
bucketsStatus,
|
||||
bucketsSelection,
|
||||
measurements,
|
||||
measurementsStatus,
|
||||
measurementsSelection,
|
||||
fields,
|
||||
fieldsStatus,
|
||||
fieldsSelection,
|
||||
functions,
|
||||
functionsSelection,
|
||||
functionsStatus,
|
||||
} = this.state
|
||||
const {tagFiltersLength, onAddTagSelector} = this.props
|
||||
|
||||
return (
|
||||
<div className="query-builder">
|
||||
<div className="query-builder--panel">
|
||||
<div className="query-builder--panel-header">Select a Bucket</div>
|
||||
<BuilderCard
|
||||
status={bucketsStatus}
|
||||
items={buckets}
|
||||
selectedItems={bucketsSelection}
|
||||
onSelectItems={this.handleSelectBuckets}
|
||||
onSearch={this.findAndSetBuckets}
|
||||
limitCount={LIMIT}
|
||||
singleSelect={true}
|
||||
/>
|
||||
<div className="query-builder--buttons">
|
||||
<Form.Element label="Bucket">
|
||||
<QueryBuilderBucketDropdown />
|
||||
</Form.Element>
|
||||
</div>
|
||||
<div className="query-builder--panel">
|
||||
<div className="query-builder--panel-header">Select Measurements</div>
|
||||
<BuilderCard
|
||||
status={measurementsStatus}
|
||||
items={measurements}
|
||||
selectedItems={measurementsSelection}
|
||||
onSelectItems={this.handleSelectMeasurement}
|
||||
onSearch={this.findAndSetMeasurements}
|
||||
emptyText={EMPTY_MEASUREMENTS_MESSAGE}
|
||||
limitCount={LIMIT}
|
||||
/>
|
||||
</div>
|
||||
<div className="query-builder--panel">
|
||||
<div className="query-builder--panel-header">
|
||||
Select Fields
|
||||
<small>Optional</small>
|
||||
</div>
|
||||
<BuilderCard
|
||||
status={fieldsStatus}
|
||||
items={fields}
|
||||
selectedItems={fieldsSelection}
|
||||
onSelectItems={this.handleSelectFields}
|
||||
onSearch={this.findAndSetFields}
|
||||
emptyText={EMPTY_FIELDS_MESSAGE}
|
||||
limitCount={LIMIT}
|
||||
/>
|
||||
</div>
|
||||
<div className="query-builder--panel">
|
||||
<div className="query-builder--panel-header">
|
||||
Select Functions
|
||||
<small>Optional</small>
|
||||
</div>
|
||||
<BuilderCard
|
||||
status={functionsStatus}
|
||||
items={functions}
|
||||
selectedItems={functionsSelection}
|
||||
onSelectItems={this.handleSelectFunctions}
|
||||
onSearch={this.handleSearchFunctions}
|
||||
emptyText={EMPTY_FUNCTIONS_MESSAGE}
|
||||
limitCount={LIMIT}
|
||||
/>
|
||||
<div className="query-builder--cards">
|
||||
<FancyScrollbar>
|
||||
<div className="query-builder--tag-selectors">
|
||||
{range(tagFiltersLength).map(i => (
|
||||
<TagSelector key={i} index={i} />
|
||||
))}
|
||||
<Button
|
||||
shape={ButtonShape.Square}
|
||||
icon={IconFont.Plus}
|
||||
onClick={onAddTagSelector}
|
||||
customClass="query-builder--add-tag-selector"
|
||||
/>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
<FunctionSelector />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private findAndSetBuckets = async (
|
||||
searchTerm: string = ''
|
||||
): Promise<void> => {
|
||||
const {queryURL, sourceType} = this.props
|
||||
|
||||
this.setState({bucketsStatus: RemoteDataState.Loading})
|
||||
|
||||
try {
|
||||
const buckets = await this.findBuckets(queryURL, sourceType, searchTerm)
|
||||
const {bucketsSelection} = this.state
|
||||
|
||||
this.setState({
|
||||
buckets: mergeUnique(buckets, bucketsSelection),
|
||||
bucketsStatus: RemoteDataState.Done,
|
||||
})
|
||||
} catch (e) {
|
||||
if (e instanceof CancellationError) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({bucketsStatus: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
private handleSelectBuckets = (bucketsSelection: string[]) => {
|
||||
if (bucketsSelection.length) {
|
||||
this.setState({bucketsSelection}, () => {
|
||||
this.findAndSetMeasurements()
|
||||
this.emitConfig()
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
bucketsSelection: [],
|
||||
measurements: [],
|
||||
measurementsStatus: RemoteDataState.NotStarted,
|
||||
measurementsSelection: [],
|
||||
fields: [],
|
||||
fieldsStatus: RemoteDataState.NotStarted,
|
||||
fieldsSelection: [],
|
||||
functionsStatus: RemoteDataState.NotStarted,
|
||||
functionsSelection: [],
|
||||
},
|
||||
this.emitConfig
|
||||
)
|
||||
}
|
||||
|
||||
private findAndSetMeasurements = async (
|
||||
searchTerm: string = ''
|
||||
): Promise<void> => {
|
||||
const {queryURL, sourceType} = this.props
|
||||
const [selectedBucket] = this.state.bucketsSelection
|
||||
|
||||
this.setState({measurementsStatus: RemoteDataState.Loading})
|
||||
|
||||
try {
|
||||
const measurements = await this.findMeasurements(
|
||||
queryURL,
|
||||
sourceType,
|
||||
selectedBucket,
|
||||
searchTerm
|
||||
)
|
||||
|
||||
const {measurementsSelection} = this.state
|
||||
|
||||
this.setState({
|
||||
measurements: mergeUnique(measurements, measurementsSelection),
|
||||
measurementsStatus: RemoteDataState.Done,
|
||||
})
|
||||
} catch (e) {
|
||||
if (e instanceof CancellationError) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({measurementsStatus: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
private handleSelectMeasurement = (measurementsSelection: string[]) => {
|
||||
if (measurementsSelection.length) {
|
||||
this.setState(
|
||||
{
|
||||
measurementsSelection,
|
||||
functionsStatus: RemoteDataState.Done,
|
||||
},
|
||||
() => {
|
||||
this.findAndSetFields()
|
||||
this.emitConfig()
|
||||
}
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
measurementsSelection: [],
|
||||
fields: [],
|
||||
fieldsStatus: RemoteDataState.NotStarted,
|
||||
fieldsSelection: [],
|
||||
functionsStatus: RemoteDataState.NotStarted,
|
||||
functionsSelection: [],
|
||||
},
|
||||
this.emitConfig
|
||||
)
|
||||
}
|
||||
|
||||
private findAndSetFields = async (searchTerm: string = ''): Promise<void> => {
|
||||
const {queryURL, sourceType} = this.props
|
||||
const {measurementsSelection} = this.state
|
||||
const [selectedBucket] = this.state.bucketsSelection
|
||||
|
||||
this.setState({fieldsStatus: RemoteDataState.Loading})
|
||||
|
||||
try {
|
||||
const fields = await this.findFields(
|
||||
queryURL,
|
||||
sourceType,
|
||||
selectedBucket,
|
||||
measurementsSelection,
|
||||
searchTerm
|
||||
)
|
||||
|
||||
const {fieldsSelection} = this.state
|
||||
|
||||
this.setState({
|
||||
fields: mergeUnique(fields, fieldsSelection),
|
||||
fieldsStatus: RemoteDataState.Done,
|
||||
})
|
||||
} catch (e) {
|
||||
if (e instanceof CancellationError) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({fieldsStatus: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
private handleSelectFields = (fieldsSelection: string[]) => {
|
||||
this.setState({fieldsSelection}, this.emitConfig)
|
||||
}
|
||||
|
||||
private handleSelectFunctions = (functionsSelection: string[]) => {
|
||||
this.setState({functionsSelection}, this.emitConfig)
|
||||
}
|
||||
|
||||
private handleSearchFunctions = async (searchTerm: string) => {
|
||||
const {functionsSelection} = this.state
|
||||
|
||||
const functions = FUNCTIONS.map(f => f.name).filter(name =>
|
||||
name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
this.setState({functions: mergeUnique(functions, functionsSelection)})
|
||||
}
|
||||
|
||||
private emitConfig = (): void => {
|
||||
const {onBuildQuery} = this.props
|
||||
const {
|
||||
bucketsSelection,
|
||||
measurementsSelection,
|
||||
fieldsSelection,
|
||||
functionsSelection,
|
||||
} = this.state
|
||||
|
||||
const config: BuilderConfig = {
|
||||
buckets: bucketsSelection,
|
||||
measurements: measurementsSelection,
|
||||
fields: fieldsSelection,
|
||||
functions: functionsSelection,
|
||||
}
|
||||
|
||||
onBuildQuery(config)
|
||||
}
|
||||
}
|
||||
|
||||
const mstp = (state: AppState): StateProps => {
|
||||
const sources = getSources(state)
|
||||
const activeSource = getActiveSource(state)
|
||||
const activeQuery = getActiveQuery(state)
|
||||
const tagFiltersLength = getActiveQuery(state).builderConfig.tags.length
|
||||
|
||||
let source: Source
|
||||
|
||||
if (activeQuery.sourceID) {
|
||||
source = sources.find(source => source.id === activeQuery.sourceID)
|
||||
} else {
|
||||
source = activeSource
|
||||
}
|
||||
|
||||
const queryURL = source.links.query
|
||||
const sourceType = source.type
|
||||
|
||||
return {queryURL, sourceType}
|
||||
return {tagFiltersLength}
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
onBuildQuery: buildQuery,
|
||||
onLoadBuckets: loadBuckets as any,
|
||||
onAddTagSelector: addTagSelector,
|
||||
}
|
||||
|
||||
export default connect<StateProps, DispatchProps>(
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
// Libraries
|
||||
import React, {SFC} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {get} from 'lodash'
|
||||
|
||||
// Components
|
||||
import {Dropdown, ComponentSize} from 'src/clockface'
|
||||
|
||||
// Actions
|
||||
import {setQuerySource} from 'src/shared/actions/v2/timeMachines'
|
||||
|
||||
// Utils
|
||||
import {getActiveTimeMachine} from 'src/shared/selectors/timeMachines'
|
||||
import {getSources} from 'src/sources/selectors'
|
||||
|
||||
// Types
|
||||
import {AppState, Source} from 'src/types/v2'
|
||||
|
||||
const DYNAMIC_SOURCE = {id: '', name: 'Dynamic Source'}
|
||||
const DROPDOWN_WIDTH = 200
|
||||
|
||||
interface StateProps {
|
||||
sources: Source[]
|
||||
selectedSourceID: string | null
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
onSelectSource: (sourceID: string) => void
|
||||
}
|
||||
|
||||
type Props = StateProps & DispatchProps
|
||||
|
||||
const TimeMachineSourceDropdown: SFC<Props> = props => {
|
||||
const {sources, selectedSourceID, onSelectSource} = props
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
selectedID={selectedSourceID}
|
||||
onChange={onSelectSource}
|
||||
buttonSize={ComponentSize.Small}
|
||||
widthPixels={DROPDOWN_WIDTH}
|
||||
>
|
||||
{[DYNAMIC_SOURCE, ...sources].map(source => (
|
||||
<Dropdown.Item key={source.id} id={source.id} value={source.id}>
|
||||
{source.name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
const mstp = (state: AppState) => {
|
||||
const sources = getSources(state)
|
||||
const {activeQueryIndex, view} = getActiveTimeMachine(state)
|
||||
const selectedSourceID: string = get(
|
||||
view,
|
||||
`properties.queries.${activeQueryIndex}.sourceID`,
|
||||
''
|
||||
)
|
||||
|
||||
return {sources, selectedSourceID}
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
onSelectSource: setQuerySource,
|
||||
}
|
||||
|
||||
export default connect<StateProps, DispatchProps, {}>(
|
||||
mstp,
|
||||
mdtp
|
||||
)(TimeMachineSourceDropdown)
|
|
@ -113,7 +113,7 @@ class CellComponent extends Component<Props> {
|
|||
} = this.props
|
||||
|
||||
return (
|
||||
<Conditional isRendered={viewStatus !== RemoteDataState.Done}>
|
||||
<Conditional isRendered={viewStatus === RemoteDataState.Done}>
|
||||
<ViewComponent
|
||||
view={view}
|
||||
onZoom={onZoom}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
export const CONFIRM_LEAVE_ADVANCED_MODE =
|
||||
'You will lose any edits you have made to your query. Are you sure?'
|
||||
'You will lose any manual edits you have made to your query. Are you sure?'
|
||||
|
|
|
@ -9,15 +9,14 @@ import {
|
|||
// Actions
|
||||
import {
|
||||
submitScript,
|
||||
setQuerySource,
|
||||
setActiveTab,
|
||||
setActiveTimeMachine,
|
||||
setActiveQueryIndex,
|
||||
setActiveQueryIndexSync,
|
||||
editActiveQueryWithBuilder,
|
||||
editActiveQueryAsFlux,
|
||||
editActiveQueryAsInfluxQL,
|
||||
addQuery,
|
||||
removeQuery,
|
||||
addQuerySync,
|
||||
removeQuerySync,
|
||||
updateActiveQueryName,
|
||||
setBackgroundThresholdColoring,
|
||||
setTextThresholdColoring,
|
||||
|
@ -80,24 +79,14 @@ describe('timeMachinesReducer', () => {
|
|||
type: InfluxLanguage.InfluxQL,
|
||||
sourceID: '123',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
{
|
||||
text: 'bar',
|
||||
type: InfluxLanguage.Flux,
|
||||
sourceID: '456',
|
||||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -127,12 +116,7 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.Flux,
|
||||
sourceID: '123',
|
||||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
}
|
||||
|
||||
const queryB: DashboardQuery = {
|
||||
|
@ -140,12 +124,7 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.Flux,
|
||||
sourceID: '456',
|
||||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
}
|
||||
|
||||
state.view.properties.queries = [queryA, queryB]
|
||||
|
@ -160,18 +139,6 @@ describe('timeMachineReducer', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('SET_QUERY_SOURCE', () => {
|
||||
test('replaces the sourceID for the active query', () => {
|
||||
const state = initialStateHelper()
|
||||
|
||||
expect(state.draftQueries[0].sourceID).toEqual('')
|
||||
|
||||
const nextState = timeMachineReducer(state, setQuerySource('howdy'))
|
||||
|
||||
expect(nextState.draftQueries[0].sourceID).toEqual('howdy')
|
||||
})
|
||||
})
|
||||
|
||||
describe('EDIT_ACTIVE_QUERY_WITH_BUILDER', () => {
|
||||
test('changes the activeQueryEditor and editMode for the currently active query', () => {
|
||||
const state = initialStateHelper()
|
||||
|
@ -183,24 +150,14 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
{
|
||||
text: 'bar',
|
||||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -213,12 +170,7 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
{
|
||||
text: '',
|
||||
|
@ -227,8 +179,7 @@ describe('timeMachineReducer', () => {
|
|||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
tags: [{key: '_measurement', values: []}],
|
||||
functions: [],
|
||||
},
|
||||
},
|
||||
|
@ -247,24 +198,14 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.InfluxQL,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
{
|
||||
text: 'bar',
|
||||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -277,24 +218,14 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.InfluxQL,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
{
|
||||
text: 'bar',
|
||||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
@ -311,24 +242,14 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.InfluxQL,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
{
|
||||
text: 'bar',
|
||||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -341,24 +262,14 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.InfluxQL,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
{
|
||||
text: 'bar',
|
||||
type: InfluxLanguage.InfluxQL,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
@ -376,28 +287,18 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
{
|
||||
text: 'bar',
|
||||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
]
|
||||
|
||||
const nextState = timeMachineReducer(state, setActiveQueryIndex(0))
|
||||
const nextState = timeMachineReducer(state, setActiveQueryIndexSync(0))
|
||||
|
||||
expect(nextState.activeQueryIndex).toEqual(0)
|
||||
})
|
||||
|
@ -412,28 +313,18 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.InfluxQL,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
{
|
||||
text: 'bar',
|
||||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
]
|
||||
|
||||
const nextState = timeMachineReducer(state, setActiveQueryIndex(0))
|
||||
const nextState = timeMachineReducer(state, setActiveQueryIndexSync(0))
|
||||
|
||||
expect(nextState.activeQueryIndex).toEqual(0)
|
||||
})
|
||||
|
@ -448,28 +339,18 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
{
|
||||
text: 'bar',
|
||||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
]
|
||||
|
||||
const nextState = timeMachineReducer(state, setActiveQueryIndex(0))
|
||||
const nextState = timeMachineReducer(state, setActiveQueryIndexSync(0))
|
||||
|
||||
expect(nextState.activeQueryIndex).toEqual(0)
|
||||
})
|
||||
|
@ -487,16 +368,11 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
]
|
||||
|
||||
const nextState = timeMachineReducer(state, addQuery())
|
||||
const nextState = timeMachineReducer(state, addQuerySync())
|
||||
|
||||
expect(nextState.activeQueryIndex).toEqual(1)
|
||||
expect(nextState.draftQueries).toEqual([
|
||||
|
@ -505,12 +381,7 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
{
|
||||
text: '',
|
||||
|
@ -519,8 +390,7 @@ describe('timeMachineReducer', () => {
|
|||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
tags: [{key: '_measurement', values: []}],
|
||||
functions: [],
|
||||
},
|
||||
},
|
||||
|
@ -538,36 +408,21 @@ describe('timeMachineReducer', () => {
|
|||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
{
|
||||
text: 'b',
|
||||
type: InfluxLanguage.Flux,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
{
|
||||
text: 'c',
|
||||
type: InfluxLanguage.InfluxQL,
|
||||
sourceID: '',
|
||||
editMode: QueryEditMode.Advanced,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
},
|
||||
builderConfig: {buckets: [], tags: [], functions: []},
|
||||
},
|
||||
]
|
||||
})
|
||||
|
@ -579,9 +434,8 @@ describe('timeMachineReducer', () => {
|
|||
state.draftQueries = queries
|
||||
state.activeQueryIndex = 1
|
||||
|
||||
const nextState = timeMachineReducer(state, removeQuery(1))
|
||||
const nextState = timeMachineReducer(state, removeQuerySync(1))
|
||||
|
||||
expect(nextState.view.properties.queries).toEqual(queries)
|
||||
expect(nextState.draftQueries).toEqual([queries[0], queries[2]])
|
||||
expect(nextState.activeQueryIndex).toEqual(1)
|
||||
})
|
||||
|
@ -593,9 +447,8 @@ describe('timeMachineReducer', () => {
|
|||
state.draftQueries = queries
|
||||
state.activeQueryIndex = 2
|
||||
|
||||
const nextState = timeMachineReducer(state, removeQuery(2))
|
||||
const nextState = timeMachineReducer(state, removeQuerySync(2))
|
||||
|
||||
expect(nextState.view.properties.queries).toEqual(queries)
|
||||
expect(nextState.draftQueries).toEqual([queries[0], queries[1]])
|
||||
expect(nextState.activeQueryIndex).toEqual(1)
|
||||
})
|
||||
|
@ -606,12 +459,7 @@ describe('timeMachineReducer', () => {
|
|||
const state = initialStateHelper()
|
||||
state.activeQueryIndex = 1
|
||||
|
||||
const builderConfig = {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
functions: [],
|
||||
}
|
||||
const builderConfig = {buckets: [], tags: [], functions: []}
|
||||
|
||||
state.draftQueries = [
|
||||
{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Libraries
|
||||
import {get, cloneDeep} from 'lodash'
|
||||
import {produce} from 'immer'
|
||||
|
||||
// Utils
|
||||
import {createView, defaultViewQuery} from 'src/shared/utils/view'
|
||||
|
@ -17,6 +18,7 @@ import {
|
|||
View,
|
||||
ViewType,
|
||||
DashboardQuery,
|
||||
BuilderConfig,
|
||||
InfluxLanguage,
|
||||
QueryEditMode,
|
||||
QueryView,
|
||||
|
@ -25,6 +27,18 @@ import {
|
|||
} from 'src/types/v2/dashboards'
|
||||
import {Action} from 'src/shared/actions/v2/timeMachines'
|
||||
import {TimeMachineTab} from 'src/types/v2/timeMachine'
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
||||
interface QueryBuilderState {
|
||||
buckets: string[]
|
||||
bucketsStatus: RemoteDataState
|
||||
tags: Array<{
|
||||
keys: string[]
|
||||
keysStatus: RemoteDataState
|
||||
values: string[]
|
||||
valuesStatus: RemoteDataState
|
||||
}>
|
||||
}
|
||||
|
||||
export interface TimeMachineState {
|
||||
view: QueryView
|
||||
|
@ -34,6 +48,7 @@ export interface TimeMachineState {
|
|||
activeTab: TimeMachineTab
|
||||
activeQueryIndex: number | null
|
||||
submitToken: number
|
||||
queryBuilder: QueryBuilderState
|
||||
}
|
||||
|
||||
export interface TimeMachinesState {
|
||||
|
@ -51,6 +66,11 @@ export const initialStateHelper = (): TimeMachineState => ({
|
|||
activeTab: TimeMachineTab.Queries,
|
||||
activeQueryIndex: 0,
|
||||
submitToken: 0,
|
||||
queryBuilder: {
|
||||
buckets: [],
|
||||
bucketsStatus: RemoteDataState.NotStarted,
|
||||
tags: [],
|
||||
},
|
||||
})
|
||||
|
||||
export const initialState = (): TimeMachinesState => ({
|
||||
|
@ -70,6 +90,7 @@ export const timeMachinesReducer = (
|
|||
const activeTimeMachine = state.timeMachines[activeTimeMachineID]
|
||||
const view = initialState.view || activeTimeMachine.view
|
||||
const draftQueries = cloneDeep(view.properties.queries)
|
||||
const queryBuilder = initialQueryBuilderState(draftQueries[0].builderConfig)
|
||||
const activeQueryIndex = 0
|
||||
|
||||
return {
|
||||
|
@ -83,6 +104,7 @@ export const timeMachinesReducer = (
|
|||
activeTab: TimeMachineTab.Queries,
|
||||
activeQueryIndex,
|
||||
draftQueries,
|
||||
queryBuilder,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -122,32 +144,11 @@ export const timeMachineReducer = (
|
|||
}
|
||||
|
||||
case 'SET_TIME_RANGE': {
|
||||
const {timeRange} = action.payload
|
||||
const {view} = state
|
||||
return produce(state, draftState => {
|
||||
draftState.timeRange = action.payload.timeRange
|
||||
|
||||
const rebuildConfig = query => ({
|
||||
...query,
|
||||
text: buildQuery(query.builderConfig, timeRange.duration),
|
||||
buildAndSubmitAllQueries(draftState)
|
||||
})
|
||||
|
||||
const draftQueries = state.draftQueries.map(rebuildConfig)
|
||||
const queries = view.properties.queries.map(rebuildConfig)
|
||||
|
||||
const newView = {
|
||||
...view,
|
||||
properties: {
|
||||
...view.properties,
|
||||
queries,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
timeRange,
|
||||
view: newView,
|
||||
draftQueries,
|
||||
submitToken: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
case 'SET_VIEW_TYPE': {
|
||||
|
@ -170,19 +171,9 @@ export const timeMachineReducer = (
|
|||
}
|
||||
|
||||
case 'SUBMIT_SCRIPT': {
|
||||
const {view, draftQueries} = state
|
||||
|
||||
return {
|
||||
...state,
|
||||
submitToken: Date.now(),
|
||||
view: {
|
||||
...view,
|
||||
properties: {
|
||||
...view.properties,
|
||||
queries: draftQueries,
|
||||
},
|
||||
},
|
||||
}
|
||||
return produce(state, draftState => {
|
||||
submitQueries(draftState)
|
||||
})
|
||||
}
|
||||
|
||||
case 'SET_IS_VIEWING_RAW_DATA': {
|
||||
|
@ -342,20 +333,6 @@ export const timeMachineReducer = (
|
|||
return setViewProperties(state, {staticLegend})
|
||||
}
|
||||
|
||||
case 'SET_QUERY_SOURCE': {
|
||||
const {sourceID} = action.payload
|
||||
const {activeQueryIndex} = state
|
||||
|
||||
const draftQueries = [...state.draftQueries]
|
||||
|
||||
draftQueries[activeQueryIndex] = {
|
||||
...draftQueries[activeQueryIndex],
|
||||
sourceID,
|
||||
}
|
||||
|
||||
return {...state, draftQueries}
|
||||
}
|
||||
|
||||
case 'INCREMENT_SUBMIT_TOKEN': {
|
||||
return {
|
||||
...state,
|
||||
|
@ -408,59 +385,186 @@ export const timeMachineReducer = (
|
|||
}
|
||||
|
||||
case 'SET_ACTIVE_QUERY_INDEX': {
|
||||
const {activeQueryIndex} = action.payload
|
||||
return produce(state, draftState => {
|
||||
const {activeQueryIndex} = action.payload
|
||||
|
||||
return {...state, activeQueryIndex}
|
||||
draftState.activeQueryIndex = activeQueryIndex
|
||||
|
||||
resetBuilderState(draftState)
|
||||
})
|
||||
}
|
||||
|
||||
case 'ADD_QUERY': {
|
||||
const draftQueries = [...state.draftQueries, defaultViewQuery()]
|
||||
const activeQueryIndex: number = draftQueries.length - 1
|
||||
return produce(state, draftState => {
|
||||
draftState.draftQueries = [...state.draftQueries, defaultViewQuery()]
|
||||
draftState.activeQueryIndex = draftState.draftQueries.length - 1
|
||||
|
||||
return {...state, activeQueryIndex, draftQueries}
|
||||
resetBuilderState(draftState)
|
||||
})
|
||||
}
|
||||
|
||||
case 'REMOVE_QUERY': {
|
||||
const {queryIndex} = action.payload
|
||||
const draftQueries = state.draftQueries.filter(
|
||||
(__, i) => i !== queryIndex
|
||||
)
|
||||
const queryLength = draftQueries.length
|
||||
return produce(state, draftState => {
|
||||
const {queryIndex} = action.payload
|
||||
|
||||
let activeQueryIndex: number
|
||||
draftState.draftQueries.splice(queryIndex, 1)
|
||||
|
||||
if (queryIndex < queryLength) {
|
||||
activeQueryIndex = queryIndex
|
||||
} else if (queryLength === queryIndex && queryLength > 0) {
|
||||
activeQueryIndex = queryLength - 1
|
||||
} else {
|
||||
activeQueryIndex = 0
|
||||
}
|
||||
const queryLength = draftState.draftQueries.length
|
||||
|
||||
return {...state, activeQueryIndex, draftQueries}
|
||||
let activeQueryIndex: number
|
||||
|
||||
if (queryIndex < queryLength) {
|
||||
activeQueryIndex = queryIndex
|
||||
} else if (queryLength === queryIndex && queryLength > 0) {
|
||||
activeQueryIndex = queryLength - 1
|
||||
} else {
|
||||
activeQueryIndex = 0
|
||||
}
|
||||
|
||||
draftState.activeQueryIndex = activeQueryIndex
|
||||
|
||||
resetBuilderState(draftState)
|
||||
submitQueries(draftState)
|
||||
})
|
||||
}
|
||||
|
||||
case 'BUILD_QUERY': {
|
||||
const {builderConfig} = action.payload
|
||||
const {activeQueryIndex, timeRange} = state
|
||||
const draftQueries = [...state.draftQueries]
|
||||
case 'SET_BUILDER_BUCKET_SELECTION': {
|
||||
return produce(state, draftState => {
|
||||
const draftQuery = draftState.draftQueries[draftState.activeQueryIndex]
|
||||
|
||||
let text: string
|
||||
|
||||
if (!isConfigValid(builderConfig)) {
|
||||
text = ''
|
||||
} else {
|
||||
text = buildQuery(builderConfig, timeRange.duration)
|
||||
}
|
||||
|
||||
draftQueries[activeQueryIndex] = {
|
||||
...draftQueries[activeQueryIndex],
|
||||
text,
|
||||
builderConfig,
|
||||
}
|
||||
|
||||
return {...state, draftQueries}
|
||||
draftQuery.builderConfig.buckets = [action.payload.bucket]
|
||||
})
|
||||
}
|
||||
|
||||
case 'SET_BUILDER_BUCKETS': {
|
||||
return produce(state, draftState => {
|
||||
draftState.queryBuilder.buckets = action.payload.buckets
|
||||
draftState.queryBuilder.bucketsStatus = RemoteDataState.Done
|
||||
})
|
||||
}
|
||||
|
||||
case 'SET_BUILDER_BUCKETS_STATUS': {
|
||||
return produce(state, draftState => {
|
||||
draftState.queryBuilder.bucketsStatus = action.payload.bucketsStatus
|
||||
})
|
||||
}
|
||||
|
||||
case 'SET_BUILDER_TAG_KEYS': {
|
||||
return produce(state, draftState => {
|
||||
const {index, keys} = action.payload
|
||||
|
||||
draftState.queryBuilder.tags[index].keys = keys
|
||||
draftState.queryBuilder.tags[index].keysStatus = RemoteDataState.Done
|
||||
})
|
||||
}
|
||||
|
||||
case 'SET_BUILDER_TAG_KEYS_STATUS': {
|
||||
return produce(state, draftState => {
|
||||
const {index, status} = action.payload
|
||||
const tags = draftState.queryBuilder.tags
|
||||
|
||||
tags[index].keysStatus = status
|
||||
|
||||
if (status === RemoteDataState.Loading) {
|
||||
for (let i = index + 1; i < tags.length; i++) {
|
||||
tags[i].keysStatus = RemoteDataState.NotStarted
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
case 'SET_BUILDER_TAG_VALUES': {
|
||||
return produce(state, draftState => {
|
||||
const {index, values} = action.payload
|
||||
|
||||
draftState.queryBuilder.tags[index].values = values
|
||||
draftState.queryBuilder.tags[index].valuesStatus = RemoteDataState.Done
|
||||
})
|
||||
}
|
||||
|
||||
case 'SET_BUILDER_TAG_VALUES_STATUS': {
|
||||
return produce(state, draftState => {
|
||||
const {index, status} = action.payload
|
||||
|
||||
draftState.queryBuilder.tags[index].valuesStatus = status
|
||||
})
|
||||
}
|
||||
|
||||
case 'SET_BUILDER_TAG_KEY_SELECTION': {
|
||||
return produce(state, draftState => {
|
||||
const {index, key} = action.payload
|
||||
const draftQuery = draftState.draftQueries[draftState.activeQueryIndex]
|
||||
const tag = draftQuery.builderConfig.tags[index]
|
||||
|
||||
tag.key = key
|
||||
tag.values = []
|
||||
})
|
||||
}
|
||||
|
||||
case 'SET_BUILDER_TAG_VALUES_SELECTION': {
|
||||
return produce(state, draftState => {
|
||||
const {index, values} = action.payload
|
||||
const draftQuery = draftState.draftQueries[draftState.activeQueryIndex]
|
||||
|
||||
draftQuery.builderConfig.tags[index].values = values
|
||||
|
||||
buildAndSubmitActiveQuery(draftState)
|
||||
})
|
||||
}
|
||||
|
||||
case 'ADD_TAG_SELECTOR': {
|
||||
return produce(state, draftState => {
|
||||
const draftQuery = draftState.draftQueries[draftState.activeQueryIndex]
|
||||
|
||||
draftQuery.builderConfig.tags.push({key: '', values: []})
|
||||
draftState.queryBuilder.tags.push({
|
||||
keys: [],
|
||||
keysStatus: RemoteDataState.NotStarted,
|
||||
values: [],
|
||||
valuesStatus: RemoteDataState.NotStarted,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
case 'REMOVE_TAG_SELECTOR': {
|
||||
return produce(state, draftState => {
|
||||
const {index} = action.payload
|
||||
const draftQuery = draftState.draftQueries[draftState.activeQueryIndex]
|
||||
|
||||
const selectedValues = draftQuery.builderConfig.tags[index].values
|
||||
|
||||
draftQuery.builderConfig.tags.splice(index, 1)
|
||||
draftState.queryBuilder.tags.splice(index, 1)
|
||||
|
||||
if (selectedValues.length) {
|
||||
buildAndSubmitActiveQuery(draftState)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
case 'SELECT_BUILDER_FUNCTION': {
|
||||
return produce(state, draftState => {
|
||||
const {name} = action.payload
|
||||
const functions =
|
||||
draftState.draftQueries[draftState.activeQueryIndex].builderConfig
|
||||
.functions
|
||||
|
||||
let newFunctions
|
||||
|
||||
if (functions.find(f => f.name === name)) {
|
||||
newFunctions = functions.filter(f => f.name !== name)
|
||||
} else {
|
||||
newFunctions = [...functions, {name}]
|
||||
}
|
||||
|
||||
draftState.draftQueries[
|
||||
draftState.activeQueryIndex
|
||||
].builderConfig.functions = newFunctions
|
||||
|
||||
buildAndSubmitActiveQuery(draftState)
|
||||
})
|
||||
}
|
||||
|
||||
case 'UPDATE_ACTIVE_QUERY_NAME': {
|
||||
const {activeQueryIndex} = state
|
||||
const {queryName} = action.payload
|
||||
|
@ -484,6 +588,7 @@ export const timeMachineReducer = (
|
|||
|
||||
return {...state, view}
|
||||
}
|
||||
|
||||
case 'SET_TABLE_OPTIONS': {
|
||||
const workingView = state.view as ExtractWorkingView<
|
||||
typeof action.payload
|
||||
|
@ -549,3 +654,62 @@ const convertView = (
|
|||
|
||||
return newView
|
||||
}
|
||||
|
||||
const initialQueryBuilderState = (
|
||||
builderConfig: BuilderConfig
|
||||
): QueryBuilderState => {
|
||||
return {
|
||||
buckets: builderConfig.buckets,
|
||||
bucketsStatus: RemoteDataState.NotStarted,
|
||||
tags: builderConfig.tags.map(_ => ({
|
||||
keys: [],
|
||||
keysStatus: RemoteDataState.NotStarted,
|
||||
values: [],
|
||||
valuesStatus: RemoteDataState.NotStarted,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
const buildAndSubmitActiveQuery = (draftState: TimeMachineState) => {
|
||||
const draftQuery = draftState.draftQueries[draftState.activeQueryIndex]
|
||||
|
||||
if (isConfigValid(draftQuery.builderConfig)) {
|
||||
draftQuery.text = buildQuery(
|
||||
draftQuery.builderConfig,
|
||||
draftState.timeRange.duration
|
||||
)
|
||||
} else {
|
||||
draftQuery.text = ''
|
||||
}
|
||||
|
||||
submitQueries(draftState)
|
||||
}
|
||||
|
||||
const buildAndSubmitAllQueries = (draftState: TimeMachineState) => {
|
||||
draftState.draftQueries
|
||||
.filter(query => query.editMode === QueryEditMode.Builder)
|
||||
.forEach(query => {
|
||||
if (isConfigValid(query.builderConfig)) {
|
||||
query.text = buildQuery(
|
||||
query.builderConfig,
|
||||
draftState.timeRange.duration
|
||||
)
|
||||
} else {
|
||||
query.text = ''
|
||||
}
|
||||
})
|
||||
|
||||
submitQueries(draftState)
|
||||
}
|
||||
|
||||
const resetBuilderState = (draftState: TimeMachineState) => {
|
||||
const newBuilderConfig =
|
||||
draftState.draftQueries[draftState.activeQueryIndex].builderConfig
|
||||
|
||||
draftState.queryBuilder = initialQueryBuilderState(newBuilderConfig)
|
||||
}
|
||||
|
||||
const submitQueries = (draftState: TimeMachineState) => {
|
||||
draftState.submitToken = Date.now()
|
||||
draftState.view.properties.queries = draftState.draftQueries
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import {AppState, DashboardQuery} from 'src/types/v2'
|
||||
import {AppState, DashboardQuery, Source} from 'src/types/v2'
|
||||
|
||||
import {getSources} from 'src/sources/selectors'
|
||||
|
||||
export const getActiveTimeMachine = (state: AppState) => {
|
||||
const {activeTimeMachineID, timeMachines} = state.timeMachines
|
||||
|
@ -12,3 +14,9 @@ export const getActiveQuery = (state: AppState): DashboardQuery => {
|
|||
|
||||
return draftQueries[activeQueryIndex]
|
||||
}
|
||||
|
||||
export const getActiveQuerySource = (state: AppState): Source => {
|
||||
// We only support the self source for now, but in the future the active
|
||||
// query may be using some other source or the “dynamic source”
|
||||
return getSources(state).find(s => s.type === Source.TypeEnum.Self)
|
||||
}
|
||||
|
|
|
@ -3,11 +3,10 @@ import {buildQuery} from 'src/shared/utils/queryBuilder'
|
|||
import {BuilderConfig} from 'src/types/v2'
|
||||
|
||||
describe('buildQuery', () => {
|
||||
test('single bucket, single measurement', () => {
|
||||
test('single tag', () => {
|
||||
const config: BuilderConfig = {
|
||||
buckets: ['b0'],
|
||||
measurements: ['m0'],
|
||||
fields: [],
|
||||
tags: [{key: '_measurement', values: ['m0']}],
|
||||
functions: [],
|
||||
}
|
||||
|
||||
|
@ -20,11 +19,13 @@ describe('buildQuery', () => {
|
|||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
test('single bucket, multiple measurements, multiple fields', () => {
|
||||
test('multiple tags', () => {
|
||||
const config: BuilderConfig = {
|
||||
buckets: ['b0'],
|
||||
measurements: ['m0', 'm1'],
|
||||
fields: ['f0', 'f1'],
|
||||
tags: [
|
||||
{key: '_measurement', values: ['m0', 'm1']},
|
||||
{key: '_field', values: ['f0', 'f1']},
|
||||
],
|
||||
functions: [],
|
||||
}
|
||||
|
||||
|
@ -38,12 +39,11 @@ describe('buildQuery', () => {
|
|||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
||||
test('multiple buckets, single measurement, multiple functions', () => {
|
||||
test('single tag, multiple functions', () => {
|
||||
const config: BuilderConfig = {
|
||||
buckets: ['b0', 'b1'],
|
||||
measurements: ['m0'],
|
||||
fields: [],
|
||||
functions: ['mean', 'median'],
|
||||
buckets: ['b0'],
|
||||
tags: [{key: '_measurement', values: ['m0']}],
|
||||
functions: [{name: 'mean'}, {name: 'median'}],
|
||||
}
|
||||
|
||||
const expected = `from(bucket: "b0")
|
||||
|
@ -55,23 +55,6 @@ describe('buildQuery', () => {
|
|||
|> yield(name: "mean")
|
||||
|
||||
from(bucket: "b0")
|
||||
|> range(start: -1h)
|
||||
|> filter(fn: (r) => r._measurement == "m0")
|
||||
|> window(every: 1m)
|
||||
|> toFloat()
|
||||
|> median()
|
||||
|> group(columns: ["_value", "_time", "_start", "_stop"], mode: "except")
|
||||
|> yield(name: "median")
|
||||
|
||||
from(bucket: "b1")
|
||||
|> range(start: -1h)
|
||||
|> filter(fn: (r) => r._measurement == "m0")
|
||||
|> window(every: 1m)
|
||||
|> mean()
|
||||
|> group(columns: ["_value", "_time", "_start", "_stop"], mode: "except")
|
||||
|> yield(name: "mean")
|
||||
|
||||
from(bucket: "b1")
|
||||
|> range(start: -1h)
|
||||
|> filter(fn: (r) => r._measurement == "m0")
|
||||
|> window(every: 1m)
|
||||
|
|
|
@ -28,8 +28,11 @@ export const timeRangeVariables = (
|
|||
}
|
||||
|
||||
export function isConfigValid(builderConfig: BuilderConfig): boolean {
|
||||
const {buckets, measurements} = builderConfig
|
||||
const isConfigValid = buckets.length >= 1 && measurements.length >= 1
|
||||
const {buckets, tags} = builderConfig
|
||||
const isConfigValid =
|
||||
buckets.length >= 1 &&
|
||||
tags.length >= 1 &&
|
||||
tags.some(({key, values}) => key && values.length > 0)
|
||||
|
||||
return isConfigValid
|
||||
}
|
||||
|
@ -38,64 +41,72 @@ export function buildQuery(
|
|||
builderConfig: BuilderConfig,
|
||||
duration: string = '1h'
|
||||
): string {
|
||||
const {buckets, measurements, fields, functions} = builderConfig
|
||||
const {functions} = builderConfig
|
||||
|
||||
let bucketFunctionPairs: Array<[string, string]>
|
||||
let query: string
|
||||
|
||||
if (functions.length) {
|
||||
bucketFunctionPairs = [].concat(
|
||||
...buckets.map(b => functions.map(f => [b, f]))
|
||||
)
|
||||
query = functions
|
||||
.map(f => buildQueryHelper(builderConfig, duration, f))
|
||||
.join('\n\n')
|
||||
} else {
|
||||
bucketFunctionPairs = buckets.map(b => [b, null] as [string, string])
|
||||
query = buildQueryHelper(builderConfig, duration)
|
||||
}
|
||||
|
||||
const query = bucketFunctionPairs
|
||||
.map(([b, f]) => buildQueryHelper(b, measurements, fields, f, duration))
|
||||
.join('\n\n')
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
function buildQueryHelper(
|
||||
bucket: string,
|
||||
measurements: string[],
|
||||
fields: string[],
|
||||
functionName: string,
|
||||
duration: string
|
||||
builderConfig: BuilderConfig,
|
||||
duration: string,
|
||||
fn?: BuilderConfig['functions'][0]
|
||||
): string {
|
||||
let query = `from(bucket: "${bucket}")
|
||||
|> range(start: -${duration})`
|
||||
const [bucket] = builderConfig.buckets
|
||||
const tagFilterCall = formatTagFilterCall(builderConfig.tags)
|
||||
const fnCall = fn ? formatFunctionCall(fn, duration) : ''
|
||||
|
||||
if (measurements.length) {
|
||||
const measurementsPredicate = measurements
|
||||
.map(m => `r._measurement == "${m}"`)
|
||||
.join(' or ')
|
||||
|
||||
query += `
|
||||
|> filter(fn: (r) => ${measurementsPredicate})`
|
||||
}
|
||||
|
||||
if (fields.length) {
|
||||
const fieldsPredicate = fields.map(f => `r._field == "${f}"`).join(' or ')
|
||||
|
||||
query += `
|
||||
|> filter(fn: (r) => ${fieldsPredicate})`
|
||||
}
|
||||
|
||||
const fn = FUNCTIONS.find(f => f.name === functionName)
|
||||
|
||||
if (fn && fn.aggregate) {
|
||||
query += `
|
||||
|> window(every: ${WINDOW_INTERVALS[duration] || DEFAULT_WINDOW_INTERVAL})
|
||||
${fn.flux}
|
||||
|> group(columns: ["_value", "_time", "_start", "_stop"], mode: "except")
|
||||
|> yield(name: "${fn.name}")`
|
||||
} else if (fn) {
|
||||
query += `
|
||||
${fn.flux}
|
||||
|> yield(name: "${fn.name}")`
|
||||
}
|
||||
const query = `from(bucket: "${bucket}")
|
||||
|> range(start: -${duration})${tagFilterCall}${fnCall}`
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
export function formatFunctionCall(
|
||||
fn: BuilderConfig['functions'][0],
|
||||
duration: string
|
||||
) {
|
||||
const fnSpec = FUNCTIONS.find(f => f.name === fn.name)
|
||||
|
||||
let fnCall: string = ''
|
||||
|
||||
if (fnSpec && fnSpec.aggregate) {
|
||||
fnCall = `
|
||||
|> window(every: ${WINDOW_INTERVALS[duration] || DEFAULT_WINDOW_INTERVAL})
|
||||
${fnSpec.flux}
|
||||
|> group(columns: ["_value", "_time", "_start", "_stop"], mode: "except")
|
||||
|> yield(name: "${fn.name}")`
|
||||
} else {
|
||||
fnCall = `
|
||||
${fnSpec.flux}
|
||||
|> yield(name: "${fn.name}")`
|
||||
}
|
||||
|
||||
return fnCall
|
||||
}
|
||||
|
||||
export function formatTagFilterCall(tagsSelections: BuilderConfig['tags']) {
|
||||
if (!tagsSelections.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const calls = tagsSelections
|
||||
.filter(({key, values}) => key && values.length)
|
||||
.map(({key, values}) => {
|
||||
const fnBody = values.map(value => `r.${key} == "${value}"`).join(' or ')
|
||||
|
||||
return `|> filter(fn: (r) => ${fnBody})`
|
||||
})
|
||||
.join('\n ')
|
||||
|
||||
return `\n ${calls}`
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import {ComponentStatus} from 'src/clockface'
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
||||
export const toComponentStatus = (status: RemoteDataState): ComponentStatus => {
|
||||
if (status === RemoteDataState.NotStarted) {
|
||||
return ComponentStatus.Disabled
|
||||
}
|
||||
|
||||
if (status === RemoteDataState.Loading) {
|
||||
return ComponentStatus.Loading
|
||||
}
|
||||
|
||||
if (status === RemoteDataState.Error) {
|
||||
return ComponentStatus.Error
|
||||
}
|
||||
|
||||
return ComponentStatus.Default
|
||||
}
|
|
@ -33,8 +33,7 @@ export function defaultViewQuery(): DashboardQuery {
|
|||
editMode: QueryEditMode.Builder,
|
||||
builderConfig: {
|
||||
buckets: [],
|
||||
measurements: [],
|
||||
fields: [],
|
||||
tags: [{key: '_measurement', values: []}],
|
||||
functions: [],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -48,9 +48,8 @@ export enum QueryEditMode {
|
|||
|
||||
export interface BuilderConfig {
|
||||
buckets: string[]
|
||||
measurements: string[]
|
||||
fields: string[]
|
||||
functions: string[]
|
||||
tags: Array<{key: string; values: string[]}>
|
||||
functions: Array<{name: string}>
|
||||
}
|
||||
|
||||
export interface DashboardQuery {
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
],
|
||||
"no-empty": false,
|
||||
"jsx-no-lambda": false,
|
||||
"max-classes-per-file": false,
|
||||
"no-empty-interface": false,
|
||||
"no-shadowed-variable": false,
|
||||
"prefer-for-of": false,
|
||||
|
|
12
view.go
12
view.go
|
@ -428,10 +428,14 @@ type DashboardQuery struct {
|
|||
}
|
||||
|
||||
type BuilderConfig struct {
|
||||
Buckets []string `json:"buckets"`
|
||||
Measurements []string `json:"measurements"`
|
||||
Fields []string `json:"fields"`
|
||||
Functions []string `json:"functions"`
|
||||
Buckets []string `json:"buckets"`
|
||||
Tags []struct {
|
||||
Key string `json:"key"`
|
||||
Values []string `json:"values"`
|
||||
} `json:"tags"`
|
||||
Functions []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"functions"`
|
||||
}
|
||||
|
||||
// Axis represents the visible extents of a visualization
|
||||
|
|
Loading…
Reference in New Issue