feat(ui/query-builder): add ability to select tags in query builder
parent
f4500008a3
commit
6a171b0217
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'},
|
||||
]
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,3 @@
|
|||
font-weight: 500;
|
||||
src: url('./fonts/RobotoMono-Medium.ttf');
|
||||
}
|
||||
|
||||
$default-font: 'Roboto', Helvetica, sans-serif;
|
||||
$code-font: 'RobotoMono', monospace;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue