diff --git a/CHANGELOG.md b/CHANGELOG.md index eefa165643..2e57600117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ### Features +1. [#5852](https://github.com/influxdata/chronograf/pull/5852): Add Flux Query Builder. +1. [#5858](https://github.com/influxdata/chronograf/pull/5858): Use time range in flux Schema Explorer. + ### Bug Fixes ### Other diff --git a/ui/src/flux/components/DatabaseList.tsx b/ui/src/flux/components/DatabaseList.tsx index af9ce8a6ab..f5f36f5bd0 100644 --- a/ui/src/flux/components/DatabaseList.tsx +++ b/ui/src/flux/components/DatabaseList.tsx @@ -3,13 +3,14 @@ import React, {PureComponent} from 'react' import DatabaseListItem from 'src/flux/components/DatabaseListItem' import {ErrorHandling} from 'src/shared/decorators/errors' -import {Source, NotificationAction} from 'src/types' +import {Source, NotificationAction, TimeRange} from 'src/types' import {executeQuery} from 'src/shared/apis/flux/query' import {parseResponse} from 'src/shared/parsing/flux/response' import {isEqual} from 'lodash' interface Props { source: Source + timeRange: TimeRange notify: NotificationAction } @@ -71,11 +72,17 @@ class DatabaseList extends PureComponent { public render() { const {databases} = this.state - const {source, notify} = this.props + const {source, timeRange, notify} = this.props return databases.map(db => { return ( - + ) }) } diff --git a/ui/src/flux/components/DatabaseListItem.tsx b/ui/src/flux/components/DatabaseListItem.tsx index ead463a468..b10941b7bc 100644 --- a/ui/src/flux/components/DatabaseListItem.tsx +++ b/ui/src/flux/components/DatabaseListItem.tsx @@ -8,12 +8,13 @@ import SchemaExplorerTree from 'src/flux/components/SchemaExplorerTree' import {OpenState} from 'src/flux/constants/explorer' // Types -import {Source, NotificationAction} from 'src/types' +import {Source, NotificationAction, TimeRange} from 'src/types' import SchemaItemCategories from 'src/flux/components/SchemaItemCategories' interface Props { db: string source: Source + timeRange: TimeRange notify: NotificationAction } @@ -49,7 +50,7 @@ class DatabaseListItem extends PureComponent { } private get categories(): JSX.Element { - const {db, source, notify} = this.props + const {db, source, timeRange, notify} = this.props const {opened} = this.state const isOpen = opened === OpenState.OPENED const isUnopen = opened === OpenState.UNOPENED @@ -57,11 +58,17 @@ class DatabaseListItem extends PureComponent { if (!isUnopen) { return (
- + {tree => ( diff --git a/ui/src/flux/components/FetchFields.tsx b/ui/src/flux/components/FetchFields.tsx index 4ad0151f26..b58a22003a 100644 --- a/ui/src/flux/components/FetchFields.tsx +++ b/ui/src/flux/components/FetchFields.tsx @@ -2,14 +2,14 @@ import {PureComponent} from 'react' // Utils -import {fieldsByMeasurement as fetchFieldsByMeasurementAsync} from 'src/shared/apis/flux/metaQueries' -import {parseFieldsByMeasurements} from 'src/shared/parsing/flux/values' +import {fetchFieldsByMeasurement} from 'src/shared/apis/flux/metaQueries' // Types -import {Source, RemoteDataState} from 'src/types' +import {Source, RemoteDataState, TimeRange} from 'src/types' interface Props { source: Source + timeRange: TimeRange bucket: string children: (fields, fieldsByMeasurement, fieldsLoading) => JSX.Element } @@ -43,13 +43,13 @@ class FetchFields extends PureComponent { } private async fetchFields() { - const {source, bucket} = this.props + const {source, timeRange, bucket} = this.props this.setState({loading: RemoteDataState.Loading}) try { - const fieldsResults = await fetchFieldsByMeasurementAsync(source, bucket) - - const {fields, fieldsByMeasurements} = parseFieldsByMeasurements( - fieldsResults + const {fields, fieldsByMeasurements} = await fetchFieldsByMeasurement( + source, + timeRange, + bucket ) this.setState({ diff --git a/ui/src/flux/components/FetchMeasurements.tsx b/ui/src/flux/components/FetchMeasurements.tsx index 96059dfe16..7075ee9869 100644 --- a/ui/src/flux/components/FetchMeasurements.tsx +++ b/ui/src/flux/components/FetchMeasurements.tsx @@ -2,14 +2,14 @@ import {PureComponent} from 'react' // Utils -import {measurements as fetchMeasurementsAsync} from 'src/shared/apis/flux/metaQueries' -import parseValuesColumn from 'src/shared/parsing/flux/values' +import {fetchMeasurements} from 'src/shared/apis/flux/metaQueries' // Types -import {Source, RemoteDataState} from 'src/types' +import {Source, RemoteDataState, TimeRange} from 'src/types' interface Props { source: Source + timeRange: TimeRange bucket: string children: (measurements, measurementsLoading) => JSX.Element } @@ -19,14 +19,6 @@ interface State { loading: RemoteDataState } -export async function fetchFluxMeasurements( - source: Source, - bucket: string -): Promise { - const measurementResults = await fetchMeasurementsAsync(source, bucket) - return parseValuesColumn(measurementResults) -} - class FetchMeasurements extends PureComponent { constructor(props) { super(props) @@ -36,18 +28,18 @@ class FetchMeasurements extends PureComponent { } } public componentDidMount() { - this.fetchMeasurements() + this.fetchData() } public render() { return this.props.children(this.state.measurements, this.state.loading) } - private async fetchMeasurements() { - const {source, bucket} = this.props + private async fetchData() { + const {source, timeRange, bucket} = this.props this.setState({loading: RemoteDataState.Loading}) try { - const measurements = await fetchFluxMeasurements(source, bucket) + const measurements = await fetchMeasurements(source, timeRange, bucket) this.setState({measurements, loading: RemoteDataState.Done}) } catch (error) { this.setState({loading: RemoteDataState.Error}) diff --git a/ui/src/flux/components/FetchTagKeys.tsx b/ui/src/flux/components/FetchTagKeys.tsx index f8c4b5650d..7be15632b1 100644 --- a/ui/src/flux/components/FetchTagKeys.tsx +++ b/ui/src/flux/components/FetchTagKeys.tsx @@ -2,14 +2,14 @@ import {PureComponent} from 'react' // Utils -import parseValuesColumn from 'src/shared/parsing/flux/values' -import {tagKeys as fetchTagKeysAsync} from 'src/shared/apis/flux/metaQueries' +import {fetchTagKeys} from 'src/shared/apis/flux/metaQueries' // Types -import {Source, RemoteDataState} from 'src/types' +import {Source, RemoteDataState, TimeRange} from 'src/types' interface Props { source: Source + timeRange: TimeRange bucket: string children: (tagKeys, tagsLoading) => JSX.Element } @@ -29,21 +29,18 @@ class FetchTagKeys extends PureComponent { } public componentDidMount() { - this.fetchTagKeys() + this.fetchData() } public render() { return this.props.children(this.state.tagKeys, this.state.loading) } - private async fetchTagKeys() { - const {source, bucket} = this.props + private async fetchData() { + const {source, timeRange, bucket} = this.props this.setState({loading: RemoteDataState.Loading}) try { - const tagKeysResults = await fetchTagKeysAsync(source, bucket, []) - - const tagKeys = parseValuesColumn(tagKeysResults) - + const tagKeys = await fetchTagKeys(source, timeRange, bucket) this.setState({tagKeys, loading: RemoteDataState.Done}) } catch (error) { this.setState({loading: RemoteDataState.Error}) diff --git a/ui/src/flux/components/FieldList.tsx b/ui/src/flux/components/FieldList.tsx index 2306b7ab3f..7d25bad6d1 100644 --- a/ui/src/flux/components/FieldList.tsx +++ b/ui/src/flux/components/FieldList.tsx @@ -94,7 +94,9 @@ class FieldList extends PureComponent { return (
-
No more fields.
+
{`No ${ + term ? 'matching ' : '' + }fields in the selected time range.`}
) diff --git a/ui/src/flux/components/MeasurementList.tsx b/ui/src/flux/components/MeasurementList.tsx index cb2d16a6ce..b42c8909f3 100644 --- a/ui/src/flux/components/MeasurementList.tsx +++ b/ui/src/flux/components/MeasurementList.tsx @@ -65,6 +65,7 @@ class MeasurementsList extends PureComponent { const {source, db, notify, loading} = this.props const {searchTerm} = this.state const measurementEntries = Object.entries(this.props.measurements) + measurementEntries.sort((a, b) => a[0].localeCompare(b[0])) if (loading === RemoteDataState.Error) { return ( @@ -127,7 +128,11 @@ class MeasurementsList extends PureComponent { return (
-
No more measurements.
+
+ {`No ${ + term ? 'matching ' : '' + }measurements in the selected time range.`} +
) diff --git a/ui/src/flux/components/SchemaExplorer.tsx b/ui/src/flux/components/SchemaExplorer.tsx index 944dfd4f25..1b1acf67e7 100644 --- a/ui/src/flux/components/SchemaExplorer.tsx +++ b/ui/src/flux/components/SchemaExplorer.tsx @@ -2,20 +2,21 @@ import React, {PureComponent} from 'react' import DatabaseList from 'src/flux/components/DatabaseList' import FancyScrollbar from 'src/shared/components/FancyScrollbar' -import {Source, NotificationAction} from 'src/types' +import {Source, NotificationAction, TimeRange} from 'src/types' interface Props { source: Source + timeRange: TimeRange notify: NotificationAction } class SchemaExplorer extends PureComponent { public render() { - const {source, notify} = this.props + const {source, timeRange, notify} = this.props return (
- +
) diff --git a/ui/src/flux/components/SchemaExplorerTree.tsx b/ui/src/flux/components/SchemaExplorerTree.tsx index 775641fe9a..fee27488a2 100644 --- a/ui/src/flux/components/SchemaExplorerTree.tsx +++ b/ui/src/flux/components/SchemaExplorerTree.tsx @@ -9,11 +9,12 @@ import FetchTagKeys from 'src/flux/components/FetchTagKeys' import FetchFields from 'src/flux/components/FetchFields' // Types -import {Source, RemoteDataState} from 'src/types' +import {Source, RemoteDataState, TimeRange} from 'src/types' interface Props { bucket: string source: Source + timeRange: TimeRange children: (tree: CategoryTree) => JSX.Element } @@ -33,14 +34,18 @@ export interface CategoryTree { @ErrorHandling class SchemaExplorerTree extends PureComponent { public render() { - const {source, bucket} = this.props + const {source, timeRange, bucket} = this.props return ( - + {(measurements, measurementsLoading) => ( - + {(tagKeys, tagsLoading) => ( - + {(fields, fieldsByMeasurements, fieldsLoading) => this.props.children( this.tree( diff --git a/ui/src/flux/components/SchemaItemCategories.tsx b/ui/src/flux/components/SchemaItemCategories.tsx index fcbe943cfb..bc5173e7a6 100644 --- a/ui/src/flux/components/SchemaItemCategories.tsx +++ b/ui/src/flux/components/SchemaItemCategories.tsx @@ -8,11 +8,12 @@ import SchemaItemCategory, { import {ErrorHandling} from 'src/shared/decorators/errors' // Types -import {Source, NotificationAction} from 'src/types' +import {Source, NotificationAction, TimeRange} from 'src/types' import {CategoryTree} from 'src/flux/components/SchemaExplorerTree' interface Props { source: Source + timeRange: TimeRange db: string categoryTree: CategoryTree notify: NotificationAction @@ -21,12 +22,13 @@ interface Props { @ErrorHandling class SchemaItemCategories extends PureComponent { public render() { - const {source, db, categoryTree, notify} = this.props + const {source, timeRange, db, categoryTree, notify} = this.props return ( <> { /> { />
{this.categoryName} + + () +
{!isUnopened && ( @@ -93,7 +98,7 @@ class SchemaItemCategory extends PureComponent< } private get itemList(): JSX.Element { - const {type, db, source, notify, categoryTree} = this.props + const {type, db, timeRange, source, notify, categoryTree} = this.props switch (type) { case CategoryType.Measurements: @@ -125,6 +130,7 @@ class SchemaItemCategory extends PureComponent< { } private get tagKeys(): JSX.Element | JSX.Element[] { - const {db, source, notify, loading} = this.props + const {db, source, timeRange, notify, loading} = this.props if (loading === RemoteDataState.Error) { return ( @@ -65,6 +66,7 @@ class TagKeyList extends PureComponent { { return (
-
No more tag keys.
+
+ No tag keys in the selected time range. +
) diff --git a/ui/src/flux/components/TagKeyListItem.tsx b/ui/src/flux/components/TagKeyListItem.tsx index 14ef264d63..ef50140b55 100644 --- a/ui/src/flux/components/TagKeyListItem.tsx +++ b/ui/src/flux/components/TagKeyListItem.tsx @@ -11,11 +11,12 @@ import {ErrorHandling} from 'src/shared/decorators/errors' import {OpenState} from 'src/flux/constants/explorer' // types -import {Source, NotificationAction} from 'src/types' +import {Source, NotificationAction, TimeRange} from 'src/types' interface Props { db: string source: Source + timeRange: TimeRange tagKey: string notify: NotificationAction onAddFilter?: (value: {[k: string]: string}) => void @@ -36,7 +37,7 @@ class TagKeyListItem extends PureComponent { } public render() { - const {db, source, tagKey, notify} = this.props + const {db, source, timeRange, tagKey, notify} = this.props const {opened} = this.state const isOpen = opened === OpenState.OPENED const isUnopen = opened === OpenState.UNOPENED @@ -61,6 +62,7 @@ class TagKeyListItem extends PureComponent { void @@ -56,7 +56,7 @@ class TagValueList extends PureComponent { public async componentDidMount() { this.setState({loading: RemoteDataState.Loading}) try { - const tagValues = await this.fetchTagValues() + const tagValues = await this.fetchData() this.setState({ tagValues, loading: RemoteDataState.Done, @@ -150,7 +150,11 @@ class TagValueList extends PureComponent { return (
-
No more tag values.
+
+ {`No ${ + term ? 'matching ' : '' + }tag values in the selected time range.`} +
) @@ -166,20 +170,18 @@ class TagValueList extends PureComponent { return `Load next ${TAG_VALUES_LIMIT} values for ${tagKey}` } - private fetchTagValues = async (): Promise => { - const {source, db, tagKey} = this.props + private fetchData = async (): Promise => { + const {source, timeRange, db, tagKey} = this.props const {searchTerm, limit} = this.state - const response = await fetchTagValues({ + return await fetchTagValues({ source, + timeRange, bucket: db, tagKey, limit, searchTerm, }) - - const tagValues = parseValuesColumn(response) - return tagValues } private onSearch = (e: ChangeEvent) => { @@ -190,7 +192,7 @@ class TagValueList extends PureComponent { () => { try { this.debouncer.call(async () => { - const tagValues = await this.fetchTagValues() + const tagValues = await this.fetchData() this.setState({tagValues}) }, 50) } catch (error) { @@ -219,7 +221,7 @@ class TagValueList extends PureComponent { // eslint-disable-next-line @typescript-eslint/no-misused-promises async () => { try { - const tagValues = await this.fetchTagValues() + const tagValues = await this.fetchData() this.setState({ tagValues, loading: RemoteDataState.Done, diff --git a/ui/src/flux/helpers/rangeArguments.ts b/ui/src/flux/helpers/rangeArguments.ts new file mode 100644 index 0000000000..880fae7f31 --- /dev/null +++ b/ui/src/flux/helpers/rangeArguments.ts @@ -0,0 +1,8 @@ +import {TimeRange} from 'src/types' + +export default function rangeArguments(timeRange: TimeRange): string { + const start = timeRange.lowerFlux ? timeRange.lowerFlux : timeRange.lower + return timeRange.upper && timeRange.upper !== 'now()' + ? `start: ${start}, stop: ${timeRange.upper}` + : `start: ${start}` +} diff --git a/ui/src/flux/helpers/recordProperty.ts b/ui/src/flux/helpers/recordProperty.ts index f303c052d9..e43cd8b99f 100644 --- a/ui/src/flux/helpers/recordProperty.ts +++ b/ui/src/flux/helpers/recordProperty.ts @@ -1,6 +1,8 @@ +import fluxString from './fluxString' + export default function recordProperty(key: string) { if (key && /[^A-Za-z0-9_]/.test(key)) { - return `r.["${key}"]` + return `r.[${fluxString(key)}]` } return `r.${key}` } diff --git a/ui/src/shared/apis/flux/metaQueries.ts b/ui/src/shared/apis/flux/metaQueries.ts index 9b9da76bf6..d01f4a4bdd 100644 --- a/ui/src/shared/apis/flux/metaQueries.ts +++ b/ui/src/shared/apis/flux/metaQueries.ts @@ -1,82 +1,67 @@ import _ from 'lodash' import AJAX from 'src/utils/ajax' -import {Source, SchemaFilter} from 'src/types' -import recordProperty from 'src/flux/helpers/recordProperty' - -export const measurements = async ( +import {Source, SchemaFilter, TimeRange} from 'src/types' +import fluxString from 'src/flux/helpers/fluxString' +import parseValuesColumn, { + parseFieldsByMeasurements, +} from 'src/shared/parsing/flux/values' +import rangeArguments from 'src/flux/helpers/rangeArguments' + +export const fetchMeasurements = async ( source: Source, + timeRange: TimeRange, bucket: string -): Promise => { - const script = ` - import "influxdata/influxdb/v1" - v1.measurements(bucket:"${bucket}") - ` - - return proxy(source, script) -} - -export const fields = async ( - source: Source, - bucket: string, - filter: SchemaFilter[], - limit: number -): Promise => { - return await tagValues({ +): Promise => { + return fetchTagValues({ bucket, source, - tagKey: '_field', - limit, - filter, + tagKey: '_measurement', + limit: 0, + timeRange, }) } // Fetch all the fields and their associated measurement -export const fieldsByMeasurement = async ( +export const fetchFieldsByMeasurement = async ( source: Source, + timeRange: TimeRange, bucket: string -): Promise => { +): Promise<{ + fields: string[] + fieldsByMeasurements: {[measurement: string]: string[]} +}> => { const script = ` - from(bucket: "${bucket}") - |> range(start: -30d) + from(bucket:${fluxString(bucket)}) + |> range(${rangeArguments(timeRange)}) |> group(columns: ["_field", "_measurement"], mode: "by") |> distinct(column: "_field") |> group() |> map(fn: (r) => ({_measurement: r._measurement, _field: r._field})) ` - return proxy(source, script) + const response = await proxy(source, script) + return parseFieldsByMeasurements(response) } -export const tagKeys = async ( +export const fetchTagKeys = async ( source: Source, - bucket: string, - filter: SchemaFilter[] -): Promise => { - let tagKeyFilter = '' - - if (filter.length) { - const predicates = filter.map(({key}) => `r._value != "${key}"`) - - tagKeyFilter = `|> filter(fn: (r) => ${predicates.join(' and ')} )` - } - - const predicate = '(r) => true' - + timeRange: TimeRange, + bucket: string +): Promise => { const script = ` - import "influxdata/influxdb/v1" - v1.tagKeys( - bucket: "${bucket}", - predicate: ${predicate}, - start: -30d, - ) - ${tagKeyFilter} - ` - - return proxy(source, script) +from(bucket:${fluxString(bucket)}) + |> range(${rangeArguments(timeRange)}) + |> keys() + |> keep(columns: ["_value"]) + |> distinct()` + + const response = await proxy(source, script) + return parseValuesColumn(response) } interface TagValuesParams { source: Source + timeRange: TimeRange bucket: string tagKey: string limit: number @@ -85,64 +70,45 @@ interface TagValuesParams { count?: boolean } -export const tagValues = async ({ +export const fetchTagValues = async ({ bucket, source, tagKey, + timeRange, limit, searchTerm = '', count = false, -}: TagValuesParams): Promise => { +}: TagValuesParams): Promise => { let regexFilter = '' if (searchTerm) { - regexFilter = `|> filter(fn: (r) => ${recordProperty( - tagKey - )} =~ /${searchTerm}/)` + regexFilter = `\n |> filter(fn: (r) => r["_value"] =~ /${searchTerm}/)` } - const limitFunc = count ? '' : `|> limit(n:${limit})` - const countFunc = count ? '|> count()' : '' - - const predicate = '(r) => true' + const limitFunc = count || !limit ? '' : `\n |> limit(n:${limit})` + const countFunc = count ? '\n |> count()' : '' const script = ` - import "influxdata/influxdb/v1" - v1.tagValues( - bucket: "${bucket}", - predicate: ${predicate}, - tag: "${tagKey}", - start: -30d, - ) - ${regexFilter} - ${limitFunc} - ${countFunc} +from(bucket:${fluxString(bucket)}) + |> range(${rangeArguments(timeRange)}) + |> keep(columns: [${fluxString(tagKey)}]) + |> group() + |> distinct(column: ${fluxString( + tagKey + )})${regexFilter}${limitFunc}${countFunc} ` - return proxy(source, script) + const csvResponse = await proxy(source, script) + return parseValuesColumn(csvResponse) } -export const tagsFromMeasurement = async ( +export const proxy = async ( source: Source, - bucket: string, - measurement: string -): Promise => { - const script = ` - from(bucket:"${bucket}") - |> range(start:-30d) - |> filter(fn:(r) => r._measurement == "${measurement}") - |> group() - |> keys() - |> keep(columns: ["_value"]) - ` - - return proxy(source, script) -} - -export const proxy = async (source: Source, script: string) => { + script: string +): Promise => { const mark = encodeURIComponent('?') - const garbage = script.replace(/\s/g, '') // server cannot handle whitespace + const minimizedScript = script.replace(/\s/g, '') // server cannot handle whitespace const dialect = {annotations: ['group', 'datatype', 'default']} - const data = {query: garbage, dialect} + const data = {query: minimizedScript, dialect} const base = source.links.flux try { @@ -153,7 +119,7 @@ export const proxy = async (source: Source, script: string) => { headers: {'Content-Type': 'application/json'}, }) - return response.data + return response.data.toString() } catch (error) { handleError(error) } diff --git a/ui/src/shared/components/TimeMachine/FluxQueryMaker.tsx b/ui/src/shared/components/TimeMachine/FluxQueryMaker.tsx index 15fcec09e9..0e5dcf65fb 100644 --- a/ui/src/shared/components/TimeMachine/FluxQueryMaker.tsx +++ b/ui/src/shared/components/TimeMachine/FluxQueryMaker.tsx @@ -100,7 +100,13 @@ class FluxQueryMaker extends PureComponent { size: leftSize, headerButtons: [], menuOptions: [], - render: () => , + render: () => ( + + ), headerOrientation: HANDLE_VERTICAL, }, { diff --git a/ui/src/shared/components/TimeMachine/fluxQueryBuilder/apis/fluxQueries.ts b/ui/src/shared/components/TimeMachine/fluxQueryBuilder/apis/fluxQueries.ts index f0d6ce4f1c..fb70e6a037 100644 --- a/ui/src/shared/components/TimeMachine/fluxQueryBuilder/apis/fluxQueries.ts +++ b/ui/src/shared/components/TimeMachine/fluxQueryBuilder/apis/fluxQueries.ts @@ -6,8 +6,9 @@ import {TimeRange, Source} from 'src/types' import {CancelBox} from 'src/types/promises' import {parseResponse} from 'src/shared/parsing/flux/response' import {BuilderTagsType} from '../types' -import {formatTimeRangeArguments, tagToFlux} from '../util/generateFlux' +import {tagToFlux} from '../util/generateFlux' import fluxString from 'src/flux/helpers/fluxString' +import rangeArguments from 'src/flux/helpers/rangeArguments' const DEFAULT_TIME_RANGE: TimeRange = {lower: 'now() - 30d', lowerFlux: '-30d'} const DEFAULT_LIMIT = 200 @@ -44,7 +45,7 @@ export function findKeys({ }: FindKeysOptions): CancelBox { const tagFilter = formatTagFilter(tagsSelections) const previousKeyFilter = formatTagKeyFilterCall(tagsSelections) - const timeRangeArguments = formatTimeRangeArguments(timeRange) + const timeRangeArguments = rangeArguments(timeRange) // requires Flux package to work which we will put in the query const searchFilter = !searchTerm @@ -85,7 +86,7 @@ export function findValues({ limit = DEFAULT_LIMIT, }: FindValuesOptions): CancelBox { const tagFilter = formatTagFilter(tagsSelections) - const timeRangeArguments = formatTimeRangeArguments(timeRange) + const timeRangeArguments = rangeArguments(timeRange) // requires Flux package to work which we will put in the query const searchFilter = !searchTerm diff --git a/ui/src/shared/components/TimeMachine/fluxQueryBuilder/util/generateFlux.ts b/ui/src/shared/components/TimeMachine/fluxQueryBuilder/util/generateFlux.ts index 680530891b..26b72e2158 100644 --- a/ui/src/shared/components/TimeMachine/fluxQueryBuilder/util/generateFlux.ts +++ b/ui/src/shared/components/TimeMachine/fluxQueryBuilder/util/generateFlux.ts @@ -1,15 +1,7 @@ import fluxString from 'src/flux/helpers/fluxString' -import {TimeRange} from 'src/types' import {BuilderTagsType, QueryBuilderState} from '../types' import {AGG_WINDOW_AUTO, FUNCTIONS} from './constants' -export function formatTimeRangeArguments(timeRange: TimeRange): string { - const start = timeRange.lowerFlux ? timeRange.lowerFlux : timeRange.lower - return timeRange.upper - ? `start: ${start}, stop: ${timeRange.upper}` - : `start: ${start}` -} - export function tagToFlux(tag: BuilderTagsType) { return tag.tagValues .map(value => `r[${fluxString(tag.tagKey)}] == ${fluxString(value)}`) diff --git a/ui/src/shared/components/TimeRangeDropdown.js b/ui/src/shared/components/TimeRangeDropdown.js index 77de127664..257b54c09e 100644 --- a/ui/src/shared/components/TimeRangeDropdown.js +++ b/ui/src/shared/components/TimeRangeDropdown.js @@ -2,8 +2,6 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import moment from 'moment' -import {connect} from 'react-redux' -import _ from 'lodash' import OnClickOutside from 'shared/components/OnClickOutside' import FancyScrollbar from 'shared/components/FancyScrollbar' @@ -12,17 +10,8 @@ import CustomTimeRangeOverlay from 'shared/components/CustomTimeRangeOverlay' import {timeRanges} from 'shared/data/timeRanges' import {DROPDOWN_MENU_MAX_HEIGHT} from 'shared/constants/index' import {ErrorHandling} from 'src/shared/decorators/errors' -import {TimeZones} from 'src/types' - -const dateFormat = 'YYYY-MM-DD HH:mm' +import TimeRangeLabel from './TimeRangeLabel' const emptyTime = {lower: '', upper: ''} -const format = (t, timeZone) => { - const m = moment(t.replace(/'/g, '')) - if (timeZone === TimeZones.UTC) { - m.utc() - } - return m.format(dateFormat) -} class TimeRangeDropdown extends Component { constructor(props) { @@ -40,22 +29,6 @@ class TimeRangeDropdown extends Component { } } - findTimeRangeInputValue = ({upper, lower}) => { - if (upper && lower) { - if (upper === 'now()') { - return `${format(lower, this.props.timeZone)} - Now` - } - - return `${format(lower, this.props.timeZone)} - ${format( - upper, - this.props.timeZone - )}` - } - - const selected = timeRanges.find(range => range.lower === lower) - return selected ? selected.inputValue : 'Custom' - } - handleClickOutside = () => { this.setState({isOpen: false}) } @@ -111,7 +84,7 @@ class TimeRangeDropdown extends Component { > - {this.findTimeRangeInputValue(selected)} + @@ -181,10 +154,6 @@ TimeRangeDropdown.propTypes = { onChooseTimeRange: func.isRequired, preventCustomTimeRange: bool, page: string, - timeZone: string, } -const mstp = state => ({ - timeZone: _.get(state, ['app', 'persisted', 'timeZone']), -}) -export default connect(mstp)(OnClickOutside(ErrorHandling(TimeRangeDropdown))) +export default OnClickOutside(ErrorHandling(TimeRangeDropdown)) diff --git a/ui/src/shared/components/TimeRangeLabel.tsx b/ui/src/shared/components/TimeRangeLabel.tsx new file mode 100644 index 0000000000..d04d966833 --- /dev/null +++ b/ui/src/shared/components/TimeRangeLabel.tsx @@ -0,0 +1,37 @@ +import moment from 'moment' +import {connect} from 'react-redux' + +import {timeRanges} from 'src/shared/data/timeRanges' +import {TimeRange, TimeZones} from 'src/types' +import _ from 'lodash' + +const dateFormat = 'YYYY-MM-DD HH:mm' +const format = (t: string, timeZone: string) => { + const m = moment(t.replace(/'/g, '')) + if (timeZone === TimeZones.UTC) { + m.utc() + } + return m.format(dateFormat) +} + +interface PassedProps { + timeRange: TimeRange +} +type Props = PassedProps & ReturnType + +const TimeRangeLabel = ({timeRange: {upper, lower}, timeZone}: Props) => { + if (upper && lower) { + if (upper === 'now()') { + return `${format(lower, timeZone)} - Now` + } + + return `${format(lower, timeZone)} - ${format(upper, timeZone)}` + } + const selected = timeRanges.find(range => range.lower === lower) + return selected ? selected.inputValue : 'Custom' +} + +const mstp = (state: any) => ({ + timeZone: _.get(state, ['app', 'persisted', 'timeZone']) as TimeZones, +}) +export default connect(mstp)(TimeRangeLabel) diff --git a/ui/test/flux/helpers/rangeAguments.test.ts b/ui/test/flux/helpers/rangeAguments.test.ts new file mode 100644 index 0000000000..e5c45b29dc --- /dev/null +++ b/ui/test/flux/helpers/rangeAguments.test.ts @@ -0,0 +1,15 @@ +import rangeArguments from 'src/flux/helpers/rangeArguments' + +describe('Flux.helpers.rangeArguments', () => { + it('formats relative time range', () => { + expect(rangeArguments({lower: 'xyz', lowerFlux: '-10s'})).toBe( + 'start: -10s' + ) + }) + it('formats absolute time range', () => { + expect(rangeArguments({lower: 'a', upper: 'b'})).toBe('start: a, stop: b') + }) + it('formats absolute time range with upper now()', () => { + expect(rangeArguments({lower: 'a', upper: 'now()'})).toBe('start: a') + }) +}) diff --git a/ui/test/shared/components/TimeMachine/fluxQueryBuilder/util/generateFlux.test.ts b/ui/test/shared/components/TimeMachine/fluxQueryBuilder/util/generateFlux.test.ts index d9e2ab135a..c1ef4e59a9 100644 --- a/ui/test/shared/components/TimeMachine/fluxQueryBuilder/util/generateFlux.test.ts +++ b/ui/test/shared/components/TimeMachine/fluxQueryBuilder/util/generateFlux.test.ts @@ -4,19 +4,10 @@ import {initialState as initialAggState} from 'src/shared/components/TimeMachine import {QueryBuilderState} from 'src/shared/components/TimeMachine/fluxQueryBuilder/types' import { buildQuery, - formatTimeRangeArguments, tagToFlux, } from 'src/shared/components/TimeMachine/fluxQueryBuilder/util/generateFlux' describe('fluxQueryBuilder/util/generateFlux', () => { - test('formatTimeRangeArguments', () => { - expect(formatTimeRangeArguments({lower: 'xyz', lowerFlux: '-10s'})).toBe( - 'start: -10s' - ) - expect(formatTimeRangeArguments({lower: 'a', upper: 'b'})).toBe( - 'start: a, stop: b' - ) - }) test('tagToFlux', () => { expect( tagToFlux({