diff --git a/ui/src/logs/utils/logQuery.test.ts b/ui/src/logs/utils/logQuery.test.ts index 0712fdd5e6..772af60c86 100644 --- a/ui/src/logs/utils/logQuery.test.ts +++ b/ui/src/logs/utils/logQuery.test.ts @@ -9,7 +9,7 @@ import {oneline} from 'src/logs/utils/helpers/formatting' import {QueryConfig} from 'src/types' import {Filter, LogQuery} from 'src/types/logs' -import {InfluxLanguage} from 'src/types/v2/dashboards' +import {InfluxLanguage, SourceType} from 'src/types/v2' describe('Logs.LogQuery', () => { let config: QueryConfig @@ -39,7 +39,7 @@ describe('Logs.LogQuery', () => { const source = { id: '1', name: 'foo', - type: 'test', + type: SourceType.Self, url: 'test.local', insecureSkipVerify: false, default: true, diff --git a/ui/src/shared/apis/v2/query.ts b/ui/src/shared/apis/v2/query.ts index 64d731d41a..78248d22ec 100644 --- a/ui/src/shared/apis/v2/query.ts +++ b/ui/src/shared/apis/v2/query.ts @@ -6,7 +6,7 @@ import {URLQuery} from 'src/types/v2/dashboards' const CHECK_LIMIT_INTERVAL = 200 const MAX_ROWS = 50000 -interface ExecuteFluxQueryResult { +export interface ExecuteFluxQueryResult { csv: string didTruncate: boolean rowCount: number diff --git a/ui/src/shared/apis/v2/queryBuilder.ts b/ui/src/shared/apis/v2/queryBuilder.ts new file mode 100644 index 0000000000..e5db6b1110 --- /dev/null +++ b/ui/src/shared/apis/v2/queryBuilder.ts @@ -0,0 +1,153 @@ +// Libraries +import {get} from 'lodash' + +// APIs +import {executeQuery, ExecuteFluxQueryResult} from 'src/shared/apis/v2/query' +import {parseResponse} from 'src/shared/parsing/flux/response' + +// Types +import {SourceType, InfluxLanguage} from 'src/types/v2' + +export const SEARCH_DURATION = '30d' +export const LIMIT = 200 + +export async function findBuckets( + url: string, + sourceType: SourceType, + searchTerm?: string +) { + if (sourceType === SourceType.V1) { + throw new Error('metaqueries not yet implemented for SourceType.V1') + } + + const resp = await findBucketsFlux(url, searchTerm) + const parsed = extractCol(resp, 'name') + + return parsed +} + +export async function findMeasurements( + url: string, + sourceType: SourceType, + bucket: string, + searchTerm: string = '' +): Promise { + if (sourceType === SourceType.V1) { + throw new Error('metaqueries not yet implemented for SourceType.V1') + } + + const resp = await findMeasurementsFlux(url, bucket, searchTerm) + const parsed = extractCol(resp, '_measurement') + + return parsed +} + +export async function findFields( + url: string, + sourceType: SourceType, + bucket: string, + measurements: string[], + searchTerm: string = '' +): Promise { + if (sourceType === SourceType.V1) { + throw new Error('metaqueries not yet implemented for SourceType.V1') + } + + const resp = await findFieldsFlux(url, bucket, measurements, searchTerm) + const parsed = extractCol(resp, '_field') + + return parsed +} + +function findBucketsFlux( + url: string, + searchTerm: string +): Promise { + 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 { + let query = `from(bucket: "${bucket}") + |> range(start: -${SEARCH_DURATION})` + + if (searchTerm) { + query += ` + |> filter(fn: (r) => r._measurement =~ /(?i:${searchTerm})/)` + } + + query += ` + |> group(by: ["_measurement"]) + |> distinct(column: "_measurement") + |> group(none: true) + |> sort(columns: ["_measurement"]) + |> limit(n: ${LIMIT})` + + return executeQuery(url, query, InfluxLanguage.Flux) +} + +function findFieldsFlux( + url: string, + bucket: string, + measurements: string[], + searchTerm: string +): Promise { + 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(by: ["_field"]) + |> distinct(column: "_field") + |> group(none: true) + |> 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', []) + + if (!data.length) { + return [] + } + + const colIndex = data[0].findIndex(d => d === colName) + + if (colIndex === -1) { + throw new Error(`could not find column "${colName}" in response`) + } + + const colValues = [] + + for (let i = 1; i < data.length; i++) { + colValues.push(data[i][colIndex]) + } + + return colValues +} diff --git a/ui/src/shared/components/SelectorCard.scss b/ui/src/shared/components/BuilderCard.scss similarity index 66% rename from ui/src/shared/components/SelectorCard.scss rename to ui/src/shared/components/BuilderCard.scss index 012171163b..4fe6632bd2 100644 --- a/ui/src/shared/components/SelectorCard.scss +++ b/ui/src/shared/components/BuilderCard.scss @@ -1,34 +1,38 @@ @import "src/style/modules"; -.selector-card { +.builder-card { position: relative; overflow: hidden; - min-height: 200px; + min-height: 150px; background: $g3-castle; border-radius: 4px; display: flex; flex-direction: column; + font-size: 13px; } -.selector-card--header { +.builder-card-search-bar { margin: 10px; - flex: 0 0 0; + flex-grow: 0; + flex-shrink: 0; } -.selector-card--items { +.builder-card--items { flex: 1 1 0; overflow: scroll; } -.selector-card--item { - font-size: 13px; +.builder-card--item { font-weight: 500; - padding: 8px 16px; + padding: 5px 15px; margin-bottom: 1px; cursor: pointer; + color: $g14-chromium; + font-family: $code-font; &.selected { background: $c-pool; + color: $g18-cloud; } &.selected:hover { @@ -41,7 +45,7 @@ } } -.selector-card--empty { +.builder-card--empty { position: absolute; top: 50%; transform: translate(0, -50%); @@ -60,3 +64,10 @@ } } + +.builder-card-limit-message { + padding: 15px; + color: $g9-mountain; + text-align: center; + font-style: italic; +} diff --git a/ui/src/shared/components/BuilderCard.tsx b/ui/src/shared/components/BuilderCard.tsx new file mode 100644 index 0000000000..3e86925a3e --- /dev/null +++ b/ui/src/shared/components/BuilderCard.tsx @@ -0,0 +1,119 @@ +// 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 + status?: RemoteDataState + emptyText?: string + limitCount?: number + singleSelect?: boolean +} + +class BuilderCard extends PureComponent { + public static defaultProps: Partial = { + status: RemoteDataState.Done, + emptyText: 'None Found', + limitCount: Infinity, + singleSelect: false, + } + + public render() { + return
{this.body}
+ } + + private get body(): JSX.Element { + const {status, onSearch, emptyText} = this.props + + if (status === RemoteDataState.Error) { + return
Failed to load
+ } + + if (status === RemoteDataState.NotStarted) { + return
{emptyText}
+ } + + return ( + <> + + {this.items} + + ) + } + + private get items(): JSX.Element { + const {status, items, limitCount} = this.props + + if (status === RemoteDataState.Loading) { + return ( +
+ +
+ ) + } + + return ( + <> +
+ {items.length ? ( + + {items.map(item => ( +
+ {item} +
+ ))} +
+ ) : ( +
Nothing found
+ )} +
+ + + ) + } + + 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 diff --git a/ui/src/shared/components/BuilderCardLimitMessage.tsx b/ui/src/shared/components/BuilderCardLimitMessage.tsx new file mode 100644 index 0000000000..b2204df16f --- /dev/null +++ b/ui/src/shared/components/BuilderCardLimitMessage.tsx @@ -0,0 +1,20 @@ +import React, {SFC} from 'react' + +interface Props { + itemCount: number + limitCount: number +} + +const BuilderCardLimitMessage: SFC = ({itemCount, limitCount}) => { + if (itemCount < limitCount) { + return null + } + + return ( +
+ Showing first {limitCount} results. Use the search bar to find more. +
+ ) +} + +export default BuilderCardLimitMessage diff --git a/ui/src/shared/components/SelectorCardSearchBar.tsx b/ui/src/shared/components/BuilderCardSearchBar.tsx similarity index 56% rename from ui/src/shared/components/SelectorCardSearchBar.tsx rename to ui/src/shared/components/BuilderCardSearchBar.tsx index fe1fb1145b..c320bba6cc 100644 --- a/ui/src/shared/components/SelectorCardSearchBar.tsx +++ b/ui/src/shared/components/BuilderCardSearchBar.tsx @@ -2,13 +2,13 @@ import React, {PureComponent, ChangeEvent} from 'react' // Components -import {Input, ComponentSize, ComponentStatus} from 'src/clockface' +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 = 500 +const SEARCH_DEBOUNCE_MS = 350 interface Props { onSearch: (searchTerm: string) => Promise @@ -16,24 +16,22 @@ interface Props { interface State { searchTerm: string - status: ComponentStatus } -class SelectorCardSearchBar extends PureComponent { - public state: State = {searchTerm: '', status: ComponentStatus.Default} +class BuilderCardSearchBar extends PureComponent { + public state: State = {searchTerm: ''} private debouncer: Debouncer = new DefaultDebouncer() public render() { - const {searchTerm, status} = this.state + const {searchTerm} = this.state return ( -
+
@@ -41,9 +39,8 @@ class SelectorCardSearchBar extends PureComponent { } private handleChange = (e: ChangeEvent) => { - this.setState( - {searchTerm: e.target.value, status: ComponentStatus.Loading}, - () => this.debouncer.call(this.emitChange, SEARCH_DEBOUNCE_MS) + this.setState({searchTerm: e.target.value}, () => + this.debouncer.call(this.emitChange, SEARCH_DEBOUNCE_MS) ) } @@ -53,19 +50,14 @@ class SelectorCardSearchBar extends PureComponent { try { await onSearch(searchTerm) - - this.setState({status: ComponentStatus.Default}) } catch (e) { if (e instanceof CancellationError) { return } - this.setState({ - searchTerm: '', - status: ComponentStatus.Default, - }) + this.setState({searchTerm: ''}) } } } -export default SelectorCardSearchBar +export default BuilderCardSearchBar diff --git a/ui/src/shared/components/SelectorCard.tsx b/ui/src/shared/components/SelectorCard.tsx deleted file mode 100644 index 5435ce7339..0000000000 --- a/ui/src/shared/components/SelectorCard.tsx +++ /dev/null @@ -1,100 +0,0 @@ -// Libraries -import React, {PureComponent} from 'react' - -// Components -import SelectorCardSearchBar from 'src/shared/components/SelectorCardSearchBar' -import WaitingText from 'src/shared/components/WaitingText' -import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar' - -// Styles -import 'src/shared/components/SelectorCard.scss' - -// Types -import {RemoteDataState} from 'src/types' - -interface Props { - items: string[] - selectedItems: string[] - onSelectItems: (selection: string[]) => void - status?: RemoteDataState - onSearch?: (searchTerm: string) => Promise - emptyText?: string - className?: string -} - -class SelectorCard extends PureComponent { - public static defaultProps: Partial = { - className: '', - status: RemoteDataState.Done, - emptyText: 'None Found', - } - - public render() { - const {status, items, onSearch, emptyText} = this.props - - if (status === RemoteDataState.Loading) { - return ( -
-
- -
-
- ) - } - - if (status === RemoteDataState.Done && !items.length) { - return ( -
-
{emptyText}
-
- ) - } - - return ( -
-
- {onSearch && } -
-
- - {items.map(item => ( -
- {item} -
- ))} -
-
-
- ) - } - - private get containerClass() { - const {className} = this.props - - return `selector-card ${className}` - } - - private itemClass = (item): string => { - if (this.props.selectedItems.includes(item)) { - return 'selector-card--item selected' - } - - return 'selector-card--item' - } - - private handleToggleItem = (item: string) => (): void => { - const {selectedItems, onSelectItems} = this.props - - if (selectedItems.includes(item)) { - onSelectItems(selectedItems.filter(x => x !== item)) - } else { - onSelectItems([...selectedItems, item]) - } - } -} - -export default SelectorCard diff --git a/ui/src/shared/components/TimeMachineQueryBuilder.scss b/ui/src/shared/components/TimeMachineQueryBuilder.scss index bfd0d91ea3..00153f32ea 100644 --- a/ui/src/shared/components/TimeMachineQueryBuilder.scss +++ b/ui/src/shared/components/TimeMachineQueryBuilder.scss @@ -9,6 +9,7 @@ bottom: 0; display: flex; padding: 10px; + @include no-user-select(); } .query-builder--panel { @@ -29,11 +30,22 @@ text-transform: uppercase; font-weight: 500; font-size: 13px; - text-align: center; color: $g8-storm; + display: flex; + justify-content: center; + align-items: center; + + small { + margin-left: 10px; + padding: 2px 4px 2px 3px; + border-radius: 2px; + background-color: $g4-onyx; + font-style: italic; + font-size: 11px; + } } -.selector-card { +.builder-card { height: 100%; flex: 1 1 0; } diff --git a/ui/src/shared/components/TimeMachineQueryBuilder.tsx b/ui/src/shared/components/TimeMachineQueryBuilder.tsx index 49754696a0..4ad8b55582 100644 --- a/ui/src/shared/components/TimeMachineQueryBuilder.tsx +++ b/ui/src/shared/components/TimeMachineQueryBuilder.tsx @@ -1,114 +1,330 @@ // Libraries import React, {PureComponent} from 'react' +import {connect} from 'react-redux' // Components -import SelectorCard from 'src/shared/components/SelectorCard' +import BuilderCard from 'src/shared/components/BuilderCard' + +// APIs +import { + findBuckets, + findMeasurements, + findFields, + LIMIT, +} from 'src/shared/apis/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, Source, SourceType} from 'src/types/v2' -const DUMMY_BUCKETS = [ - 'Array', - 'Int8Array', - 'Uint8Array', - 'Uint8ClampedArray', - 'Int16Array', - 'Uint16Array', - 'Int32Array', - 'Uint32Array', - 'Float32Array', - 'Float64Array', -] +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() -interface Props {} +interface StateProps { + queryURL: string + sourceType: SourceType +} interface State { buckets: string[] bucketsStatus: RemoteDataState - selectedBuckets: string[] + bucketsSelection: string[] + measurements: string[] + measurementsStatus: RemoteDataState + measurementsSelection: string[] + fields: string[] + fieldsStatus: RemoteDataState + fieldsSelection: string[] + functions: string[] + functionsStatus: RemoteDataState + functionsSelection: string[] } -class TimeMachineQueryBuilder extends PureComponent { +class TimeMachineQueryBuilder extends PureComponent { public state: State = { - buckets: DUMMY_BUCKETS, - bucketsStatus: RemoteDataState.NotStarted, - selectedBuckets: [DUMMY_BUCKETS[0], DUMMY_BUCKETS[1]], + 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: StateProps) { + if (prevProps.queryURL !== this.props.queryURL) { + this.findAndSetBuckets() + } } public render() { - const {buckets, bucketsStatus, selectedBuckets} = this.state + const { + buckets, + bucketsStatus, + bucketsSelection, + measurements, + measurementsStatus, + measurementsSelection, + fields, + fieldsStatus, + fieldsSelection, + functions, + functionsSelection, + functionsStatus, + } = this.state return (
Select a Bucket
-
Select Measurements
- {}} - onSearch={() => Promise.resolve()} - emptyText="No measurements found" +
- Select Measurement Fields + Select Fields + Optional
- {}} - onSearch={() => Promise.resolve()} - emptyText="No fields found" +
-
Select Functions
- {}} - onSearch={() => Promise.resolve()} - emptyText="Select at least one bucket and measurement" +
+ Select Functions + Optional +
+
) } - private handleSelectBuckets = (selectedBuckets: string[]) => { - this.setState({selectedBuckets}) + private findAndSetBuckets = async ( + searchTerm: string = '' + ): Promise => { + 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 handleSearchBuckets = async (searchTerm: string) => { - await new Promise(res => setTimeout(res, 350)) - - if (searchTerm === '') { - this.setState({buckets: DUMMY_BUCKETS}) + private handleSelectBuckets = (bucketsSelection: string[]) => { + if (bucketsSelection.length) { + this.setState({bucketsSelection}, this.findAndSetMeasurements) return } this.setState({ - buckets: this.state.buckets.filter(b => { - return b.toLowerCase().includes(searchTerm.toLowerCase()) - }), + bucketsSelection: [], + measurements: [], + measurementsStatus: RemoteDataState.NotStarted, + measurementsSelection: [], + fields: [], + fieldsStatus: RemoteDataState.NotStarted, + fieldsSelection: [], + functionsStatus: RemoteDataState.NotStarted, + functionsSelection: [], }) } + + private findAndSetMeasurements = async ( + searchTerm: string = '' + ): Promise => { + 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 + ) + + return + } + + this.setState({ + measurementsSelection: [], + fields: [], + fieldsStatus: RemoteDataState.NotStarted, + fieldsSelection: [], + functionsStatus: RemoteDataState.NotStarted, + functionsSelection: [], + }) + } + + private findAndSetFields = async (searchTerm: string = ''): Promise => { + 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}) + } + + private handleSelectFunctions = (functionsSelection: string[]) => { + this.setState({functionsSelection}) + } + + 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)}) + } } -export default TimeMachineQueryBuilder +const mstp = (state: AppState): StateProps => { + const sources = getSources(state) + const activeSource = getActiveSource(state) + const activeQuery = getActiveQuery(state) + + 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} +} + +export default connect(mstp)(TimeMachineQueryBuilder) diff --git a/ui/src/shared/constants/queryBuilder.ts b/ui/src/shared/constants/queryBuilder.ts new file mode 100644 index 0000000000..d8b1de6f1d --- /dev/null +++ b/ui/src/shared/constants/queryBuilder.ts @@ -0,0 +1,21 @@ +export interface QueryFn { + name: string +} + +export const FUNCTIONS: QueryFn[] = [ + {name: 'mean'}, + {name: 'median'}, + {name: 'max'}, + {name: 'min'}, + {name: 'sum'}, + {name: 'distinct'}, + {name: 'count'}, + {name: 'increase'}, + {name: 'skew'}, + {name: 'spread'}, + {name: 'stddev'}, + {name: 'first'}, + {name: 'last'}, + {name: 'unique'}, + {name: 'sort'}, +] diff --git a/ui/src/shared/selectors/timeMachines.ts b/ui/src/shared/selectors/timeMachines.ts index b9f2a70957..7f499ad57d 100644 --- a/ui/src/shared/selectors/timeMachines.ts +++ b/ui/src/shared/selectors/timeMachines.ts @@ -1,4 +1,4 @@ -import {AppState} from 'src/types/v2' +import {AppState, DashboardQuery} from 'src/types/v2' export const getActiveTimeMachine = (state: AppState) => { const {activeTimeMachineID, timeMachines} = state.timeMachines @@ -7,6 +7,12 @@ export const getActiveTimeMachine = (state: AppState) => { return timeMachine } +export const getActiveQuery = (state: AppState): DashboardQuery => { + const {view, activeQueryIndex} = getActiveTimeMachine(state) + + return view.properties.queries[activeQueryIndex] +} + export const getActiveDraftScript = (state: AppState) => { const {draftScripts, activeQueryIndex} = getActiveTimeMachine(state) const activeDraftScript = draftScripts[activeQueryIndex] diff --git a/ui/src/sources/components/CreateSourceOverlay.tsx b/ui/src/sources/components/CreateSourceOverlay.tsx index ea5dd39dfe..f8aca6acff 100644 --- a/ui/src/sources/components/CreateSourceOverlay.tsx +++ b/ui/src/sources/components/CreateSourceOverlay.tsx @@ -26,7 +26,7 @@ import {sourceCreationFailed} from 'src/shared/copy/notifications' import 'src/sources/components/CreateSourceOverlay.scss' // Types -import {Source} from 'src/types/v2' +import {Source, SourceType} from 'src/types/v2' import {RemoteDataState} from 'src/types' interface DispatchProps { @@ -49,7 +49,7 @@ class CreateSourceOverlay extends PureComponent { public state: State = { draftSource: { name: '', - type: 'v1', + type: SourceType.V1, url: '', }, creationStatus: RemoteDataState.NotStarted, @@ -94,16 +94,16 @@ class CreateSourceOverlay extends PureComponent { v1 v2 @@ -144,7 +144,7 @@ class CreateSourceOverlay extends PureComponent { this.setState({draftSource}) } - private handleChangeType = (type: 'v1' | 'v2') => { + private handleChangeType = (type: SourceType) => { const draftSource = { ...this.state.draftSource, type, diff --git a/ui/src/sources/components/SourcesListRow.tsx b/ui/src/sources/components/SourcesListRow.tsx index 73a016f05c..bd1e1e33b6 100644 --- a/ui/src/sources/components/SourcesListRow.tsx +++ b/ui/src/sources/components/SourcesListRow.tsx @@ -20,7 +20,7 @@ import 'src/sources/components/SourcesListRow.scss' // Types import {AppState} from 'src/types/v2' -import {Source} from 'src/types/v2' +import {Source, SourceType} from 'src/types/v2' interface StateProps { activeSourceID: string @@ -43,7 +43,7 @@ const SourcesListRow: SFC = ({ onSetActiveSource, onDeleteSource, }) => { - const canDelete = source.type !== 'self' + const canDelete = source.type !== SourceType.Self const isActiveSource = source.id === activeSourceID const onButtonClick = () => onSetActiveSource(source.id) const onDeleteClick = () => onDeleteSource(source.id) diff --git a/ui/src/style/_variables.scss b/ui/src/style/_variables.scss index 6e39050f52..1471e0de5f 100644 --- a/ui/src/style/_variables.scss +++ b/ui/src/style/_variables.scss @@ -103,3 +103,6 @@ $form-lg-font: 17px; /* Empty State */ $empty-state-text: $g9-mountain; + +$default-font: 'Roboto', Helvetica, sans-serif; +$code-font: 'RobotoMono', monospace; diff --git a/ui/src/style/fonts/fonts.scss b/ui/src/style/fonts/fonts.scss index f8da38242a..0d23bc2f1b 100644 --- a/ui/src/style/fonts/fonts.scss +++ b/ui/src/style/fonts/fonts.scss @@ -40,6 +40,3 @@ font-weight: 500; src: url('./fonts/RobotoMono-Medium.ttf'); } - -$default-font: 'Roboto', Helvetica, sans-serif; -$code-font: 'RobotoMono', monospace; diff --git a/ui/src/types/v2/index.ts b/ui/src/types/v2/index.ts index d33cb0ed73..70912d787c 100644 --- a/ui/src/types/v2/index.ts +++ b/ui/src/types/v2/index.ts @@ -1,4 +1,4 @@ -import {Source} from 'src/types/v2/sources' +import {Source, SourceType} from 'src/types/v2/sources' import {Bucket, RetentionRule, RetentionRuleTypes} from 'src/types/v2/buckets' import {RangeState} from 'src/dashboards/reducers/v2/ranges' import {ViewsState} from 'src/dashboards/reducers/v2/views' @@ -12,6 +12,7 @@ import { ViewParams, ViewProperties, DashboardQuery, + InfluxLanguage, } from 'src/types/v2/dashboards' import {Cell, Dashboard} from 'src/api' @@ -56,6 +57,7 @@ export type GetState = () => AppState export { Source, + SourceType, Member, Bucket, OverlayState, @@ -77,4 +79,5 @@ export { Organization, Task, MeState, + InfluxLanguage, } diff --git a/ui/src/types/v2/sources.ts b/ui/src/types/v2/sources.ts index b447a30abd..250c425f90 100644 --- a/ui/src/types/v2/sources.ts +++ b/ui/src/types/v2/sources.ts @@ -10,7 +10,7 @@ export enum SourceAuthenticationMethod { export interface Source { id: string name: string - type: string + type: SourceType username?: string password?: string sharedSecret?: string @@ -31,3 +31,9 @@ export interface SourceLinks { buckets: string health: string } + +export enum SourceType { + V1 = 'v1', + V2 = 'v2', + Self = 'self', +}