feat(ui/query-builder): add ability to select tags in query builder

pull/10616/head
Christopher Henn 2018-12-05 09:45:45 -08:00 committed by Chris Henn
parent f4500008a3
commit 6a171b0217
18 changed files with 661 additions and 202 deletions

View File

@ -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,

View File

@ -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

View File

@ -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<string[]> {
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<string[]> {
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<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(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<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(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
}

View File

@ -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;
}

View File

@ -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<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

View File

@ -0,0 +1,20 @@
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

View File

@ -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<void>
@ -16,24 +16,22 @@ interface Props {
interface State {
searchTerm: string
status: ComponentStatus
}
class SelectorCardSearchBar extends PureComponent<Props, State> {
public state: State = {searchTerm: '', status: ComponentStatus.Default}
class BuilderCardSearchBar extends PureComponent<Props, State> {
public state: State = {searchTerm: ''}
private debouncer: Debouncer = new DefaultDebouncer()
public render() {
const {searchTerm, status} = this.state
const {searchTerm} = this.state
return (
<div className="selector-card-search-bar">
<div className="builder-card-search-bar">
<Input
size={ComponentSize.Small}
placeholder="Search..."
value={searchTerm}
status={status}
onChange={this.handleChange}
/>
</div>
@ -41,9 +39,8 @@ class SelectorCardSearchBar extends PureComponent<Props, State> {
}
private handleChange = (e: ChangeEvent<HTMLInputElement>) => {
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<Props, State> {
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

View File

@ -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<void>
emptyText?: string
className?: string
}
class SelectorCard extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
className: '',
status: RemoteDataState.Done,
emptyText: 'None Found',
}
public render() {
const {status, items, onSearch, emptyText} = this.props
if (status === RemoteDataState.Loading) {
return (
<div className={this.containerClass}>
<div className="selector-card--empty selector-card--loading">
<WaitingText text="Loading" />
</div>
</div>
)
}
if (status === RemoteDataState.Done && !items.length) {
return (
<div className={this.containerClass}>
<div className="selector-card--empty">{emptyText}</div>
</div>
)
}
return (
<div className={this.containerClass}>
<div className="selector-card--header">
{onSearch && <SelectorCardSearchBar onSearch={onSearch} />}
</div>
<div className="selector-card--items">
<FancyScrollbar>
{items.map(item => (
<div
key={item}
className={this.itemClass(item)}
onClick={this.handleToggleItem(item)}
>
{item}
</div>
))}
</FancyScrollbar>
</div>
</div>
)
}
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

View File

@ -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;
}

View File

@ -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<Props, State> {
class TimeMachineQueryBuilder extends PureComponent<StateProps, State> {
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 (
<div className="query-builder">
<div className="query-builder--panel">
<div className="query-builder--panel-header">Select a Bucket</div>
<SelectorCard
<BuilderCard
status={bucketsStatus}
items={buckets}
selectedItems={selectedBuckets}
selectedItems={bucketsSelection}
onSelectItems={this.handleSelectBuckets}
onSearch={this.handleSearchBuckets}
emptyText="No buckets found"
onSearch={this.findAndSetBuckets}
limitCount={LIMIT}
singleSelect={true}
/>
</div>
<div className="query-builder--panel">
<div className="query-builder--panel-header">Select Measurements</div>
<SelectorCard
items={[]}
selectedItems={[]}
onSelectItems={() => {}}
onSearch={() => Promise.resolve()}
emptyText="No measurements found"
<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 Measurement Fields
Select Fields
<small>Optional</small>
</div>
<SelectorCard
items={[]}
selectedItems={[]}
onSelectItems={() => {}}
onSearch={() => Promise.resolve()}
emptyText="No fields found"
<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</div>
<SelectorCard
items={[]}
selectedItems={[]}
onSelectItems={() => {}}
onSearch={() => Promise.resolve()}
emptyText="Select at least one bucket and measurement"
<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>
</div>
)
}
private handleSelectBuckets = (selectedBuckets: string[]) => {
this.setState({selectedBuckets})
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 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<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
)
return
}
this.setState({
measurementsSelection: [],
fields: [],
fieldsStatus: RemoteDataState.NotStarted,
fieldsSelection: [],
functionsStatus: RemoteDataState.NotStarted,
functionsSelection: [],
})
}
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})
}
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<StateProps>(mstp)(TimeMachineQueryBuilder)

View File

@ -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'},
]

View File

@ -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]

View File

@ -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<Props, State> {
public state: State = {
draftSource: {
name: '',
type: 'v1',
type: SourceType.V1,
url: '',
},
creationStatus: RemoteDataState.NotStarted,
@ -94,16 +94,16 @@ class CreateSourceOverlay extends PureComponent<Props, State> {
<Form.Element label="Type">
<Radio>
<Radio.Button
active={draftSource.type === 'v1'}
active={draftSource.type === SourceType.V1}
onClick={this.handleChangeType}
value="v1"
value={SourceType.V1}
>
v1
</Radio.Button>
<Radio.Button
active={draftSource.type === 'v2'}
active={draftSource.type === SourceType.V2}
onClick={this.handleChangeType}
value="v2"
value={SourceType.V2}
>
v2
</Radio.Button>
@ -144,7 +144,7 @@ class CreateSourceOverlay extends PureComponent<Props, State> {
this.setState({draftSource})
}
private handleChangeType = (type: 'v1' | 'v2') => {
private handleChangeType = (type: SourceType) => {
const draftSource = {
...this.state.draftSource,
type,

View File

@ -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<Props> = ({
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)

View File

@ -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;

View File

@ -40,6 +40,3 @@
font-weight: 500;
src: url('./fonts/RobotoMono-Medium.ttf');
}
$default-font: 'Roboto', Helvetica, sans-serif;
$code-font: 'RobotoMono', monospace;

View File

@ -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,
}

View File

@ -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',
}