mirror of https://github.com/grafana/grafana
Explore: use GrafanaTheme2 (AdHocFilter component) (#37434)
* Explore: use GrafanaTheme2 and useStyles2 instead of the old ones * Explore: delete files and components that were'nt being usedpull/37518/head
parent
e3fe4a2d11
commit
0b376522ac
@ -1,89 +0,0 @@ |
||||
import React from 'react'; |
||||
import { LegacyForms, useStyles } from '@grafana/ui'; |
||||
const { Select } = LegacyForms; |
||||
import { css, cx } from '@emotion/css'; |
||||
import { GrafanaTheme, SelectableValue } from '@grafana/data'; |
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({ |
||||
keyValueContainer: css` |
||||
label: key-value-container; |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
`,
|
||||
}); |
||||
|
||||
enum ChangeType { |
||||
Key = 'key', |
||||
Value = 'value', |
||||
Operator = 'operator', |
||||
} |
||||
|
||||
export interface Props { |
||||
keys: string[]; |
||||
keysPlaceHolder?: string; |
||||
initialKey?: string; |
||||
initialOperator?: string; |
||||
initialValue?: string; |
||||
values?: string[]; |
||||
valuesPlaceHolder?: string; |
||||
onKeyChanged: (key: string) => void; |
||||
onValueChanged: (value: string) => void; |
||||
onOperatorChanged: (operator: string) => void; |
||||
} |
||||
|
||||
export const AdHocFilter: React.FunctionComponent<Props> = (props) => { |
||||
const styles = useStyles(getStyles); |
||||
|
||||
const onChange = (changeType: ChangeType) => (item: SelectableValue<string>) => { |
||||
const { onKeyChanged, onValueChanged, onOperatorChanged } = props; |
||||
|
||||
if (!item.value) { |
||||
return; |
||||
} |
||||
|
||||
switch (changeType) { |
||||
case ChangeType.Key: |
||||
onKeyChanged(item.value); |
||||
break; |
||||
case ChangeType.Operator: |
||||
onOperatorChanged(item.value); |
||||
break; |
||||
case ChangeType.Value: |
||||
onValueChanged(item.value); |
||||
break; |
||||
} |
||||
}; |
||||
|
||||
const stringToOption = (value: string) => ({ label: value, value: value }); |
||||
|
||||
const { keys, initialKey, keysPlaceHolder, initialOperator, values, initialValue, valuesPlaceHolder } = props; |
||||
const operators = ['=', '!=']; |
||||
const keysAsOptions = keys ? keys.map(stringToOption) : []; |
||||
const selectedKey = initialKey ? keysAsOptions.filter((option) => option.value === initialKey) : undefined; |
||||
const valuesAsOptions = values ? values.map(stringToOption) : []; |
||||
const selectedValue = initialValue ? valuesAsOptions.filter((option) => option.value === initialValue) : undefined; |
||||
const operatorsAsOptions = operators.map(stringToOption); |
||||
const selectedOperator = initialOperator |
||||
? operatorsAsOptions.filter((option) => option.value === initialOperator) |
||||
: undefined; |
||||
|
||||
return ( |
||||
<div className={cx([styles.keyValueContainer])}> |
||||
<Select |
||||
options={keysAsOptions} |
||||
isSearchable |
||||
value={selectedKey} |
||||
onChange={onChange(ChangeType.Key)} |
||||
placeholder={keysPlaceHolder} |
||||
/> |
||||
<Select options={operatorsAsOptions} value={selectedOperator} onChange={onChange(ChangeType.Operator)} /> |
||||
<Select |
||||
options={valuesAsOptions} |
||||
isSearchable |
||||
value={selectedValue} |
||||
onChange={onChange(ChangeType.Value)} |
||||
placeholder={valuesPlaceHolder} |
||||
/> |
||||
</div> |
||||
); |
||||
}; |
@ -1,231 +0,0 @@ |
||||
import React from 'react'; |
||||
import { mount, shallow } from 'enzyme'; |
||||
import { DataSourceApi } from '@grafana/data'; |
||||
|
||||
import { AdHocFilterField, DEFAULT_REMOVE_FILTER_VALUE, KeyValuePair, Props } from './AdHocFilterField'; |
||||
import { AdHocFilter } from './AdHocFilter'; |
||||
import { MockDataSourceApi } from '../../../test/mocks/datasource_srv'; |
||||
|
||||
describe('<AdHocFilterField />', () => { |
||||
let mockDataSourceApi: DataSourceApi; |
||||
|
||||
beforeEach(() => { |
||||
mockDataSourceApi = new MockDataSourceApi(); |
||||
}); |
||||
|
||||
it('should initially have no filters', () => { |
||||
const mockOnPairsChanged = jest.fn(); |
||||
const wrapper = shallow(<AdHocFilterField datasource={mockDataSourceApi} onPairsChanged={mockOnPairsChanged} />); |
||||
expect(wrapper.state('pairs')).toEqual([]); |
||||
expect(wrapper.find(AdHocFilter).exists()).toBeFalsy(); |
||||
}); |
||||
|
||||
it('should add <AdHocFilter /> when onAddFilter is invoked', async () => { |
||||
const mockOnPairsChanged = jest.fn(); |
||||
const wrapper = shallow(<AdHocFilterField datasource={mockDataSourceApi} onPairsChanged={mockOnPairsChanged} />); |
||||
expect(wrapper.state('pairs')).toEqual([]); |
||||
wrapper.find('button').first().simulate('click'); |
||||
const asyncCheck = setImmediate(() => { |
||||
expect(wrapper.find(AdHocFilter).exists()).toBeTruthy(); |
||||
}); |
||||
global.clearImmediate(asyncCheck); |
||||
}); |
||||
|
||||
it(`should remove the relevant filter when the '${DEFAULT_REMOVE_FILTER_VALUE}' key is selected`, () => { |
||||
const mockOnPairsChanged = jest.fn(); |
||||
const wrapper = shallow(<AdHocFilterField datasource={mockDataSourceApi} onPairsChanged={mockOnPairsChanged} />); |
||||
expect(wrapper.state('pairs')).toEqual([]); |
||||
|
||||
wrapper.find('button').first().simulate('click'); |
||||
const asyncCheck = setImmediate(() => { |
||||
expect(wrapper.find(AdHocFilter).exists()).toBeTruthy(); |
||||
|
||||
wrapper.find(AdHocFilter).prop('onKeyChanged')(DEFAULT_REMOVE_FILTER_VALUE); |
||||
expect(wrapper.find(AdHocFilter).exists()).toBeFalsy(); |
||||
}); |
||||
global.clearImmediate(asyncCheck); |
||||
}); |
||||
|
||||
it('it should call onPairsChanged when a filter is removed', async () => { |
||||
const mockOnPairsChanged = jest.fn(); |
||||
const wrapper = shallow(<AdHocFilterField datasource={mockDataSourceApi} onPairsChanged={mockOnPairsChanged} />); |
||||
expect(wrapper.state('pairs')).toEqual([]); |
||||
|
||||
wrapper.find('button').first().simulate('click'); |
||||
const asyncCheck = setImmediate(() => { |
||||
expect(wrapper.find(AdHocFilter).exists()).toBeTruthy(); |
||||
|
||||
wrapper.find(AdHocFilter).prop('onKeyChanged')(DEFAULT_REMOVE_FILTER_VALUE); |
||||
expect(wrapper.find(AdHocFilter).exists()).toBeFalsy(); |
||||
|
||||
expect(mockOnPairsChanged.mock.calls.length).toBe(1); |
||||
}); |
||||
global.clearImmediate(asyncCheck); |
||||
}); |
||||
}); |
||||
|
||||
const setup = (propOverrides?: Partial<Props>) => { |
||||
const datasource: DataSourceApi<any, any> = ({ |
||||
getTagKeys: jest.fn().mockReturnValue([{ text: 'key 1' }, { text: 'key 2' }]), |
||||
getTagValues: jest.fn().mockReturnValue([{ text: 'value 1' }, { text: 'value 2' }]), |
||||
} as unknown) as DataSourceApi<any, any>; |
||||
|
||||
const props: Props = { |
||||
datasource, |
||||
onPairsChanged: jest.fn(), |
||||
}; |
||||
|
||||
Object.assign(props, propOverrides); |
||||
|
||||
const wrapper = mount(<AdHocFilterField {...props} />); |
||||
const instance = wrapper.instance() as AdHocFilterField; |
||||
|
||||
return { |
||||
instance, |
||||
wrapper, |
||||
datasource, |
||||
}; |
||||
}; |
||||
|
||||
describe('AdHocFilterField', () => { |
||||
describe('loadTagKeys', () => { |
||||
describe('when called and there is no extendedOptions', () => { |
||||
const { instance, datasource } = setup({ extendedOptions: undefined }); |
||||
|
||||
it('then it should return correct keys', async () => { |
||||
const keys = await instance.loadTagKeys(); |
||||
|
||||
expect(keys).toEqual(['key 1', 'key 2']); |
||||
}); |
||||
|
||||
it('then datasource.getTagKeys should be called with an empty object', async () => { |
||||
await instance.loadTagKeys(); |
||||
|
||||
expect(datasource.getTagKeys).toBeCalledWith({}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called and there is extendedOptions', () => { |
||||
const extendedOptions = { measurement: 'default' }; |
||||
const { instance, datasource } = setup({ extendedOptions }); |
||||
|
||||
it('then it should return correct keys', async () => { |
||||
const keys = await instance.loadTagKeys(); |
||||
|
||||
expect(keys).toEqual(['key 1', 'key 2']); |
||||
}); |
||||
|
||||
it('then datasource.getTagKeys should be called with extendedOptions', async () => { |
||||
await instance.loadTagKeys(); |
||||
|
||||
expect(datasource.getTagKeys).toBeCalledWith(extendedOptions); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('loadTagValues', () => { |
||||
describe('when called and there is no extendedOptions', () => { |
||||
const { instance, datasource } = setup({ extendedOptions: undefined }); |
||||
|
||||
it('then it should return correct values', async () => { |
||||
const values = await instance.loadTagValues('key 1'); |
||||
|
||||
expect(values).toEqual(['value 1', 'value 2']); |
||||
}); |
||||
|
||||
it('then datasource.getTagValues should be called with the correct key', async () => { |
||||
await instance.loadTagValues('key 1'); |
||||
|
||||
expect(datasource.getTagValues).toBeCalledWith({ key: 'key 1' }); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called and there is extendedOptions', () => { |
||||
const extendedOptions = { measurement: 'default' }; |
||||
const { instance, datasource } = setup({ extendedOptions }); |
||||
|
||||
it('then it should return correct values', async () => { |
||||
const values = await instance.loadTagValues('key 1'); |
||||
|
||||
expect(values).toEqual(['value 1', 'value 2']); |
||||
}); |
||||
|
||||
it('then datasource.getTagValues should be called with extendedOptions and the correct key', async () => { |
||||
await instance.loadTagValues('key 1'); |
||||
|
||||
expect(datasource.getTagValues).toBeCalledWith({ measurement: 'default', key: 'key 1' }); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('updatePairs', () => { |
||||
describe('when called with an empty pairs array', () => { |
||||
describe('and called with keys', () => { |
||||
it('then it should return correct pairs', async () => { |
||||
// @todo remove lint disable when possible: https://github.com/typescript-eslint/typescript-eslint/issues/902
|
||||
/* eslint-disable @typescript-eslint/no-inferrable-types */ |
||||
const { instance } = setup(); |
||||
const pairs: KeyValuePair[] = []; |
||||
const index = 0; |
||||
const key: undefined = undefined; |
||||
const keys: string[] = ['key 1', 'key 2']; |
||||
const value: undefined = undefined; |
||||
const values: undefined = undefined; |
||||
const operator: undefined = undefined; |
||||
/* eslint-enable @typescript-eslint/no-inferrable-types */ |
||||
|
||||
const result = instance.updatePairs(pairs, index, { key, keys, value, values, operator }); |
||||
|
||||
expect(result).toEqual([{ key: '', keys, value: '', values: [], operator: '' }]); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when called with an non empty pairs array', () => { |
||||
it('then it should update correct pairs at supplied index', async () => { |
||||
const { instance } = setup(); |
||||
const pairs: KeyValuePair[] = [ |
||||
{ |
||||
key: 'prev key 1', |
||||
keys: ['prev key 1', 'prev key 2'], |
||||
value: 'prev value 1', |
||||
values: ['prev value 1', 'prev value 2'], |
||||
operator: '=', |
||||
}, |
||||
{ |
||||
key: 'prev key 3', |
||||
keys: ['prev key 3', 'prev key 4'], |
||||
value: 'prev value 3', |
||||
values: ['prev value 3', 'prev value 4'], |
||||
operator: '!=', |
||||
}, |
||||
]; |
||||
const index = 1; |
||||
const key = 'key 3'; |
||||
const keys = ['key 3', 'key 4']; |
||||
const value = 'value 3'; |
||||
const values = ['value 3', 'value 4']; |
||||
const operator = '='; |
||||
|
||||
const result = instance.updatePairs(pairs, index, { key, keys, value, values, operator }); |
||||
|
||||
expect(result).toEqual([ |
||||
{ |
||||
key: 'prev key 1', |
||||
keys: ['prev key 1', 'prev key 2'], |
||||
value: 'prev value 1', |
||||
values: ['prev value 1', 'prev value 2'], |
||||
operator: '=', |
||||
}, |
||||
{ |
||||
key: 'key 3', |
||||
keys: ['key 3', 'key 4'], |
||||
value: 'value 3', |
||||
values: ['value 3', 'value 4'], |
||||
operator: '=', |
||||
}, |
||||
]); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -1,167 +0,0 @@ |
||||
import React from 'react'; |
||||
import { isEqual } from 'lodash'; |
||||
import { DataSourceApi, DataQuery, DataSourceJsonData } from '@grafana/data'; |
||||
import { Icon } from '@grafana/ui'; |
||||
import { AdHocFilter } from './AdHocFilter'; |
||||
export const DEFAULT_REMOVE_FILTER_VALUE = '-- remove filter --'; |
||||
|
||||
const addFilterButton = (onAddFilter: (event: React.MouseEvent) => void) => ( |
||||
<button className="gf-form-label gf-form-label--btn query-part" onClick={onAddFilter}> |
||||
<Icon name="plus" /> |
||||
</button> |
||||
); |
||||
|
||||
export interface KeyValuePair { |
||||
keys: string[]; |
||||
key: string; |
||||
operator: string; |
||||
value: string; |
||||
values: string[]; |
||||
} |
||||
|
||||
export interface Props<TQuery extends DataQuery = DataQuery, TOptions extends DataSourceJsonData = DataSourceJsonData> { |
||||
datasource: DataSourceApi<TQuery, TOptions>; |
||||
onPairsChanged: (pairs: KeyValuePair[]) => void; |
||||
extendedOptions?: any; |
||||
} |
||||
|
||||
export interface State { |
||||
pairs: KeyValuePair[]; |
||||
} |
||||
|
||||
export class AdHocFilterField< |
||||
TQuery extends DataQuery = DataQuery, |
||||
TOptions extends DataSourceJsonData = DataSourceJsonData |
||||
> extends React.PureComponent<Props<TQuery, TOptions>, State> { |
||||
state: State = { pairs: [] }; |
||||
|
||||
componentDidUpdate(prevProps: Props<TQuery, TOptions>) { |
||||
if (isEqual(prevProps.extendedOptions, this.props.extendedOptions) === false) { |
||||
const pairs: any[] = []; |
||||
|
||||
this.setState({ pairs }, () => this.props.onPairsChanged(pairs)); |
||||
} |
||||
} |
||||
|
||||
loadTagKeys = async () => { |
||||
const { datasource, extendedOptions } = this.props; |
||||
const options = extendedOptions || {}; |
||||
const tagKeys = datasource.getTagKeys ? await datasource.getTagKeys(options) : []; |
||||
const keys = tagKeys.map((tagKey) => tagKey.text); |
||||
|
||||
return keys; |
||||
}; |
||||
|
||||
loadTagValues = async (key: string) => { |
||||
const { datasource, extendedOptions } = this.props; |
||||
const options = extendedOptions || {}; |
||||
const tagValues = datasource.getTagValues ? await datasource.getTagValues({ ...options, key }) : []; |
||||
const values = tagValues.map((tagValue) => tagValue.text); |
||||
|
||||
return values; |
||||
}; |
||||
|
||||
updatePairs(pairs: KeyValuePair[], index: number, pair: Partial<KeyValuePair>) { |
||||
if (pairs.length === 0) { |
||||
return [ |
||||
{ |
||||
key: pair.key || '', |
||||
keys: pair.keys || [], |
||||
operator: pair.operator || '', |
||||
value: pair.value || '', |
||||
values: pair.values || [], |
||||
}, |
||||
]; |
||||
} |
||||
|
||||
const newPairs: KeyValuePair[] = []; |
||||
for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) { |
||||
const newPair = pairs[pairIndex]; |
||||
if (index === pairIndex) { |
||||
newPairs.push({ |
||||
...newPair, |
||||
key: pair.key || newPair.key, |
||||
value: pair.value || newPair.value, |
||||
operator: pair.operator || newPair.operator, |
||||
keys: pair.keys || newPair.keys, |
||||
values: pair.values || newPair.values, |
||||
}); |
||||
continue; |
||||
} |
||||
|
||||
newPairs.push(newPair); |
||||
} |
||||
|
||||
return newPairs; |
||||
} |
||||
|
||||
onKeyChanged = (index: number) => async (key: string) => { |
||||
if (key !== DEFAULT_REMOVE_FILTER_VALUE) { |
||||
const { onPairsChanged } = this.props; |
||||
const values = await this.loadTagValues(key); |
||||
const pairs = this.updatePairs(this.state.pairs, index, { key, values }); |
||||
|
||||
this.setState({ pairs }, () => onPairsChanged(pairs)); |
||||
} else { |
||||
this.onRemoveFilter(index); |
||||
} |
||||
}; |
||||
|
||||
onValueChanged = (index: number) => (value: string) => { |
||||
const pairs = this.updatePairs(this.state.pairs, index, { value }); |
||||
|
||||
this.setState({ pairs }, () => this.props.onPairsChanged(pairs)); |
||||
}; |
||||
|
||||
onOperatorChanged = (index: number) => (operator: string) => { |
||||
const pairs = this.updatePairs(this.state.pairs, index, { operator }); |
||||
|
||||
this.setState({ pairs }, () => this.props.onPairsChanged(pairs)); |
||||
}; |
||||
|
||||
onAddFilter = async () => { |
||||
const keys = await this.loadTagKeys(); |
||||
const pairs = this.state.pairs.concat(this.updatePairs([], 0, { keys })); |
||||
|
||||
this.setState({ pairs }, () => this.props.onPairsChanged(pairs)); |
||||
}; |
||||
|
||||
onRemoveFilter = async (index: number) => { |
||||
const pairs = this.state.pairs.reduce((allPairs, pair, pairIndex) => { |
||||
if (pairIndex === index) { |
||||
return allPairs; |
||||
} |
||||
return allPairs.concat(pair); |
||||
}, [] as KeyValuePair[]); |
||||
|
||||
this.setState({ pairs }); |
||||
}; |
||||
|
||||
render() { |
||||
const { pairs } = this.state; |
||||
return ( |
||||
<> |
||||
{pairs.length < 1 && addFilterButton(this.onAddFilter)} |
||||
{pairs.map((pair, index) => { |
||||
const adHocKey = `adhoc-filter-${index}-${pair.key}-${pair.value}`; |
||||
return ( |
||||
<div className="align-items-center flex-grow-1" key={adHocKey}> |
||||
<AdHocFilter |
||||
keys={[DEFAULT_REMOVE_FILTER_VALUE].concat(pair.keys)} |
||||
values={pair.values} |
||||
initialKey={pair.key} |
||||
initialOperator={pair.operator} |
||||
initialValue={pair.value} |
||||
onKeyChanged={this.onKeyChanged(index)} |
||||
onOperatorChanged={this.onOperatorChanged(index)} |
||||
onValueChanged={this.onValueChanged(index)} |
||||
/> |
||||
{index < pairs.length - 1 && <span> AND </span>} |
||||
{index === pairs.length - 1 && addFilterButton(this.onAddFilter)} |
||||
</div> |
||||
); |
||||
})} |
||||
</> |
||||
); |
||||
} |
||||
} |
@ -1,100 +0,0 @@ |
||||
import React from 'react'; |
||||
import { mount } from 'enzyme'; |
||||
import { InfluxLogsQueryField, pairsAreValid } from './InfluxLogsQueryField'; |
||||
import { InfluxDatasourceMock } from '../datasource.mock'; |
||||
import InfluxDatasource from '../datasource'; |
||||
import { InfluxQuery } from '../types'; |
||||
import { ButtonCascader } from '@grafana/ui'; |
||||
import { KeyValuePair } from '../../../../features/explore/AdHocFilterField'; |
||||
|
||||
describe('pairsAreValid()', () => { |
||||
describe('when all pairs are fully defined', () => { |
||||
it('should return true', () => { |
||||
const pairs = [ |
||||
{ |
||||
key: 'a', |
||||
operator: '=', |
||||
value: '1', |
||||
}, |
||||
{ |
||||
key: 'b', |
||||
operator: '!=', |
||||
value: '2', |
||||
}, |
||||
]; |
||||
|
||||
expect(pairsAreValid(pairs as any)).toBe(true); |
||||
}); |
||||
}); |
||||
|
||||
describe('when no pairs are defined at all', () => { |
||||
it('should return true', () => { |
||||
expect(pairsAreValid([])).toBe(true); |
||||
}); |
||||
}); |
||||
|
||||
describe('when pairs are undefined', () => { |
||||
it('should return true', () => { |
||||
expect(pairsAreValid((undefined as unknown) as KeyValuePair[])).toBe(true); |
||||
}); |
||||
}); |
||||
|
||||
describe('when one or more pairs are only partially defined', () => { |
||||
it('should return false', () => { |
||||
const pairs = [ |
||||
{ |
||||
key: 'a', |
||||
operator: undefined, |
||||
value: '1', |
||||
}, |
||||
{ |
||||
key: 'b', |
||||
operator: '!=', |
||||
value: '2', |
||||
}, |
||||
]; |
||||
|
||||
expect(pairsAreValid(pairs as any)).toBe(false); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('InfluxLogsQueryField', () => { |
||||
it('should load and show correct measurements and fields in cascader', async () => { |
||||
const wrapper = getInfluxLogsQueryField(); |
||||
// Looks strange but we do async stuff in didMount and this will push the stack at the end of eval loop, effectively
|
||||
// waiting for the didMount to finish.
|
||||
await new Promise((resolve) => setImmediate(resolve)); |
||||
wrapper.update(); |
||||
const cascader = wrapper.find(ButtonCascader); |
||||
expect(cascader.prop('options')).toEqual([ |
||||
{ label: 'logs', value: 'logs', children: [{ label: 'description', value: 'description', children: [] }] }, |
||||
]); |
||||
}); |
||||
}); |
||||
|
||||
function getInfluxLogsQueryField(props?: any) { |
||||
const datasource: InfluxDatasource = new InfluxDatasourceMock( |
||||
props?.measurements || { |
||||
logs: [{ name: 'description', type: 'string' }], |
||||
} |
||||
) as any; |
||||
|
||||
const defaultProps = { |
||||
datasource, |
||||
history: [] as any[], |
||||
onRunQuery: () => {}, |
||||
onChange: (query: InfluxQuery) => {}, |
||||
query: { |
||||
refId: '', |
||||
} as InfluxQuery, |
||||
}; |
||||
return mount( |
||||
<InfluxLogsQueryField |
||||
{...{ |
||||
...defaultProps, |
||||
...props, |
||||
}} |
||||
/> |
||||
); |
||||
} |
@ -1,167 +0,0 @@ |
||||
import React from 'react'; |
||||
import { ExploreQueryFieldProps } from '@grafana/data'; |
||||
import { ButtonCascader, CascaderOption } from '@grafana/ui'; |
||||
|
||||
import InfluxQueryModel from '../influx_query_model'; |
||||
import { AdHocFilterField, KeyValuePair } from 'app/features/explore/AdHocFilterField'; |
||||
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; |
||||
import InfluxDatasource from '../datasource'; |
||||
import { InfluxQueryBuilder } from '../query_builder'; |
||||
import { InfluxOptions, InfluxQuery } from '../types'; |
||||
|
||||
export interface Props extends ExploreQueryFieldProps<InfluxDatasource, InfluxQuery, InfluxOptions> {} |
||||
|
||||
export interface State { |
||||
measurements: CascaderOption[]; |
||||
measurement: string | null; |
||||
field: string | null; |
||||
error: string | null; |
||||
} |
||||
|
||||
interface ChooserOptions { |
||||
measurement: string | null; |
||||
field: string | null; |
||||
error: string | null; |
||||
} |
||||
|
||||
// Helper function for determining if a collection of pairs are valid
|
||||
// where a valid pair is either fully defined, or not defined at all, but not partially defined
|
||||
export function pairsAreValid(pairs: KeyValuePair[]) { |
||||
return ( |
||||
!pairs || |
||||
pairs.every((pair) => { |
||||
const allDefined = !!(pair.key && pair.operator && pair.value); |
||||
const allEmpty = pair.key === undefined && pair.operator === undefined && pair.value === undefined; |
||||
return allDefined || allEmpty; |
||||
}) |
||||
); |
||||
} |
||||
|
||||
function getChooserText({ measurement, field, error }: ChooserOptions): string { |
||||
if (error) { |
||||
return '(No measurement found)'; |
||||
} |
||||
if (measurement) { |
||||
return `Measurements (${measurement}/${field})`; |
||||
} |
||||
return 'Measurements'; |
||||
} |
||||
|
||||
export class InfluxLogsQueryField extends React.PureComponent<Props, State> { |
||||
templateSrv: TemplateSrv = getTemplateSrv(); |
||||
state: State = { |
||||
measurements: [], |
||||
measurement: null, |
||||
field: null, |
||||
error: null, |
||||
}; |
||||
|
||||
async componentDidMount() { |
||||
const { datasource } = this.props; |
||||
try { |
||||
const queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, datasource.database); |
||||
const measureMentsQuery = queryBuilder.buildExploreQuery('MEASUREMENTS'); |
||||
const influxMeasurements = await datasource.metricFindQuery(measureMentsQuery); |
||||
|
||||
const measurements = []; |
||||
for (let index = 0; index < influxMeasurements.length; index++) { |
||||
const measurementObj = influxMeasurements[index]; |
||||
const queryBuilder = new InfluxQueryBuilder( |
||||
{ measurement: measurementObj.text, tags: [] }, |
||||
datasource.database |
||||
); |
||||
const fieldsQuery = queryBuilder.buildExploreQuery('FIELDS'); |
||||
const influxFields = await datasource.metricFindQuery(fieldsQuery); |
||||
const fields: any[] = influxFields.map((field: any): any => ({ |
||||
label: field.text, |
||||
value: field.text, |
||||
children: [], |
||||
})); |
||||
measurements.push({ |
||||
label: measurementObj.text, |
||||
value: measurementObj.text, |
||||
children: fields, |
||||
}); |
||||
} |
||||
this.setState({ measurements }); |
||||
} catch (error) { |
||||
const message = error && error.message ? error.message : error; |
||||
console.error(error); |
||||
this.setState({ error: message }); |
||||
} |
||||
} |
||||
|
||||
componentDidUpdate(prevProps: Props) { |
||||
if (prevProps.query.measurement && !this.props.query.measurement) { |
||||
this.setState({ measurement: null, field: null }); |
||||
} |
||||
} |
||||
|
||||
onMeasurementsChange = async (values: string[]) => { |
||||
const { query } = this.props; |
||||
const measurement = values[0]; |
||||
const field = values[1]; |
||||
|
||||
this.setState({ measurement, field }, () => { |
||||
this.onPairsChanged((query as any).tags); |
||||
}); |
||||
}; |
||||
|
||||
onPairsChanged = (pairs: KeyValuePair[]) => { |
||||
const { query } = this.props; |
||||
const { measurement, field } = this.state; |
||||
const queryModel = new InfluxQueryModel( |
||||
{ |
||||
...query, |
||||
resultFormat: 'table', |
||||
groupBy: [], |
||||
select: [[{ type: 'field', params: [field ?? ''] }]], |
||||
tags: pairs, |
||||
limit: '1000', |
||||
measurement: measurement ?? '', |
||||
}, |
||||
this.templateSrv |
||||
); |
||||
|
||||
this.props.onChange(queryModel.target); |
||||
|
||||
// Only run the query if measurement & field are set, and there are no invalid pairs
|
||||
if (measurement && field && pairsAreValid(pairs)) { |
||||
this.props.onRunQuery(); |
||||
} |
||||
}; |
||||
|
||||
render() { |
||||
const { datasource } = this.props; |
||||
const { measurements, measurement, field, error } = this.state; |
||||
const cascadeText = getChooserText({ measurement, field, error }); |
||||
const hasMeasurement = measurements && measurements.length > 0; |
||||
|
||||
return ( |
||||
<div className="gf-form-inline gf-form-inline--nowrap"> |
||||
<div className="gf-form flex-shrink-0"> |
||||
<ButtonCascader |
||||
options={measurements} |
||||
disabled={!hasMeasurement} |
||||
value={[measurement ?? '', field ?? '']} |
||||
onChange={this.onMeasurementsChange} |
||||
> |
||||
{cascadeText} |
||||
</ButtonCascader> |
||||
</div> |
||||
<div className="flex-shrink-1 flex-flow-column-nowrap"> |
||||
{measurement && ( |
||||
<AdHocFilterField |
||||
onPairsChanged={this.onPairsChanged} |
||||
datasource={datasource} |
||||
extendedOptions={{ measurement }} |
||||
/> |
||||
)} |
||||
{error ? ( |
||||
<span className="gf-form-label gf-form-label--transparent gf-form-label--error m-l-2">{error}</span> |
||||
) : null} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
Loading…
Reference in new issue