diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index 2003a4f62fe..40c89e43850 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -1,4 +1,4 @@ -import { ComponentType, ComponentClass } from 'react'; +import { ComponentType } from 'react'; import { TimeRange, RawTimeRange, @@ -16,10 +16,9 @@ import { PluginMeta, GrafanaPlugin } from './plugin'; import { PanelData } from './panel'; import { Observable } from 'rxjs'; -// NOTE: this seems more general than just DataSource -export interface DataSourcePluginOptionsEditorProps { - options: TOptions; - onOptionsChange: (options: TOptions) => void; +export interface DataSourcePluginOptionsEditorProps { + options: DataSourceSettings; + onOptionsChange: (options: DataSourceSettings) => void; } export class DataSourcePlugin< @@ -36,7 +35,7 @@ export class DataSourcePlugin< this.components = {}; } - setConfigEditor(editor: ComponentType>>) { + setConfigEditor(editor: ComponentType>) { this.components.ConfigEditor = editor; return this; } @@ -61,22 +60,22 @@ export class DataSourcePlugin< return this; } - setExploreQueryField(ExploreQueryField: ComponentClass>) { + setExploreQueryField(ExploreQueryField: ComponentType>) { this.components.ExploreQueryField = ExploreQueryField; return this; } - setExploreMetricsQueryField(ExploreQueryField: ComponentClass>) { + setExploreMetricsQueryField(ExploreQueryField: ComponentType>) { this.components.ExploreMetricsQueryField = ExploreQueryField; return this; } - setExploreLogsQueryField(ExploreQueryField: ComponentClass>) { + setExploreLogsQueryField(ExploreQueryField: ComponentType>) { this.components.ExploreLogsQueryField = ExploreQueryField; return this; } - setExploreStartPage(ExploreStartPage: ComponentClass) { + setExploreStartPage(ExploreStartPage: ComponentType) { this.components.ExploreStartPage = ExploreStartPage; return this; } @@ -134,11 +133,11 @@ export interface DataSourcePluginComponents< AnnotationsQueryCtrl?: any; VariableQueryEditor?: any; QueryEditor?: ComponentType>; - ExploreQueryField?: ComponentClass>; - ExploreMetricsQueryField?: ComponentClass>; - ExploreLogsQueryField?: ComponentClass>; - ExploreStartPage?: ComponentClass; - ConfigEditor?: ComponentType>>; + ExploreQueryField?: ComponentType>; + ExploreMetricsQueryField?: ComponentType>; + ExploreLogsQueryField?: ComponentType>; + ExploreStartPage?: ComponentType; + ConfigEditor?: ComponentType>; } // Only exported for tests @@ -508,6 +507,7 @@ export interface DataSourceSettings; readOnly: boolean; withCredentials: boolean; + version?: number; } /** diff --git a/packages/grafana-ui/src/utils/validate.test.ts b/packages/grafana-ui/src/utils/validate.test.ts new file mode 100644 index 00000000000..50a18cfb0c3 --- /dev/null +++ b/packages/grafana-ui/src/utils/validate.test.ts @@ -0,0 +1,56 @@ +import { regexValidation, validate } from './validate'; + +describe('validate', () => { + it('passes value to the rule', () => { + expect.assertions(1); + validate('some string', [ + { + rule: (value: string) => { + expect(value).toBe('some string'); + return true; + }, + errorMessage: '', + }, + ]); + }); + + it('runs multiple validation rules that return true', () => { + expect(validate('some string', [pass(), pass(), pass()])).toEqual(null); + }); + + it('returns error message if one rule fails', () => { + expect(validate('some string', [pass(), fail('error'), pass()])).toEqual(['error']); + }); + + it('returns all error messages', () => { + expect(validate('some string', [fail('error1'), fail('error2'), fail('error3')])).toEqual([ + 'error1', + 'error2', + 'error3', + ]); + }); +}); + +describe('regexValidation', () => { + it('runs regex on a value', () => { + expect(validate('some value', [regexValidation(/some\svalu./)])).toBe(null); + }); + + it('runs fail if regex does not match', () => { + expect(validate('some value', [regexValidation(/some\svalu\d/, 'regex failed')])).toEqual(['regex failed']); + }); +}); + +const pass = () => { + return { + rule: () => true, + errorMessage: 'Should not happen', + }; +}; + +const fail = (message: string) => { + return { + rule: () => false, + errorMessage: message, + }; +}; diff --git a/packages/grafana-ui/src/utils/validate.ts b/packages/grafana-ui/src/utils/validate.ts index 286ec700577..a3ca13e23ab 100644 --- a/packages/grafana-ui/src/utils/validate.ts +++ b/packages/grafana-ui/src/utils/validate.ts @@ -22,3 +22,12 @@ export const validate = (value: string, validationRules: ValidationRule[]) => { export const hasValidationEvent = (event: EventsWithValidation, validationEvents: ValidationEvents | undefined) => { return validationEvents && validationEvents[event]; }; + +export const regexValidation = (pattern: string | RegExp, errorMessage?: string): ValidationRule => { + return { + rule: (valueToValidate: string) => { + return !!valueToValidate.match(pattern); + }, + errorMessage: errorMessage || 'Value is not valid', + }; +}; diff --git a/public/app/features/datasources/mocks.ts b/public/app/features/datasources/mocks.ts new file mode 100644 index 00000000000..88cffaeabf6 --- /dev/null +++ b/public/app/features/datasources/mocks.ts @@ -0,0 +1,23 @@ +import { DataSourceSettings } from '@grafana/ui'; + +export function createDatasourceSettings(jsonData: T): DataSourceSettings { + return { + id: 0, + orgId: 0, + name: 'datasource-test', + typeLogoUrl: '', + type: 'datasource', + access: 'server', + url: 'http://localhost', + password: '', + user: '', + database: '', + basicAuth: false, + basicAuthPassword: '', + basicAuthUser: '', + isDefault: false, + jsonData, + readOnly: false, + withCredentials: false, + }; +} diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 6482dd5c871..00232704e72 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -1,5 +1,5 @@ // Libraries -import React, { ComponentClass } from 'react'; +import React, { ComponentType } from 'react'; import { hot } from 'react-hot-loader'; import { css } from 'emotion'; import { connect } from 'react-redux'; @@ -63,7 +63,7 @@ const getStyles = memoizeOne(() => { }); interface ExploreProps { - StartPage?: ComponentClass; + StartPage?: ComponentType; changeSize: typeof changeSize; datasourceError: string; datasourceInstance: DataSourceApi; diff --git a/public/app/plugins/datasource/elasticsearch/config_ctrl.ts b/public/app/plugins/datasource/elasticsearch/config_ctrl.ts deleted file mode 100644 index 7fad7160ac6..00000000000 --- a/public/app/plugins/datasource/elasticsearch/config_ctrl.ts +++ /dev/null @@ -1,54 +0,0 @@ -import _ from 'lodash'; -import { ElasticsearchOptions } from './types'; -import { DataSourceInstanceSettings } from '@grafana/ui'; -import { getMaxConcurrenShardRequestOrDefault } from './datasource'; - -export class ElasticConfigCtrl { - static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/config.html'; - current: DataSourceInstanceSettings; - - /** @ngInject */ - constructor($scope: any) { - this.current.jsonData.timeField = this.current.jsonData.timeField || '@timestamp'; - this.current.jsonData.esVersion = this.current.jsonData.esVersion || 5; - const defaultMaxConcurrentShardRequests = this.current.jsonData.esVersion >= 70 ? 5 : 256; - this.current.jsonData.maxConcurrentShardRequests = - this.current.jsonData.maxConcurrentShardRequests || defaultMaxConcurrentShardRequests; - this.current.jsonData.logMessageField = this.current.jsonData.logMessageField || ''; - this.current.jsonData.logLevelField = this.current.jsonData.logLevelField || ''; - } - - indexPatternTypes: any = [ - { name: 'No pattern', value: undefined }, - { name: 'Hourly', value: 'Hourly', example: '[logstash-]YYYY.MM.DD.HH' }, - { name: 'Daily', value: 'Daily', example: '[logstash-]YYYY.MM.DD' }, - { name: 'Weekly', value: 'Weekly', example: '[logstash-]GGGG.WW' }, - { name: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM' }, - { name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY' }, - ]; - - esVersions = [ - { name: '2.x', value: 2 }, - { name: '5.x', value: 5 }, - { name: '5.6+', value: 56 }, - { name: '6.0+', value: 60 }, - { name: '7.0+', value: 70 }, - ]; - - indexPatternTypeChanged() { - if ( - !this.current.database || - this.current.database.length === 0 || - this.current.database.startsWith('[logstash-]') - ) { - const def: any = _.find(this.indexPatternTypes, { - value: this.current.jsonData.interval, - }); - this.current.database = def.example || 'es-index-name'; - } - } - - versionChanged() { - this.current.jsonData.maxConcurrentShardRequests = getMaxConcurrenShardRequestOrDefault(this.current.jsonData); - } -} diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx new file mode 100644 index 00000000000..1abfa583738 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { ConfigEditor } from './ConfigEditor'; +import { DataSourceHttpSettings } from '@grafana/ui'; +import { ElasticDetails } from './ElasticDetails'; +import { LogsConfig } from './LogsConfig'; +import { createDefaultConfigOptions } from './mocks'; + +describe('ConfigEditor', () => { + it('should render without error', () => { + mount( {}} options={createDefaultConfigOptions()} />); + }); + + it('should render all parts of the config', () => { + const wrapper = shallow( {}} options={createDefaultConfigOptions()} />); + expect(wrapper.find(DataSourceHttpSettings).length).toBe(1); + expect(wrapper.find(ElasticDetails).length).toBe(1); + expect(wrapper.find(LogsConfig).length).toBe(1); + }); + + it('should set defaults', () => { + const options = createDefaultConfigOptions(); + delete options.jsonData.esVersion; + delete options.jsonData.timeField; + delete options.jsonData.maxConcurrentShardRequests; + + expect.assertions(3); + + mount( + { + expect(options.jsonData.esVersion).toBe(5); + expect(options.jsonData.timeField).toBe('@timestamp'); + expect(options.jsonData.maxConcurrentShardRequests).toBe(256); + }} + options={options} + /> + ); + }); + + it('should not apply default if values are set', () => { + expect.assertions(3); + + mount( + { + expect(options.jsonData.esVersion).toBe(70); + expect(options.jsonData.timeField).toBe('@time'); + expect(options.jsonData.maxConcurrentShardRequests).toBe(300); + }} + options={createDefaultConfigOptions()} + /> + ); + }); +}); diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx new file mode 100644 index 00000000000..cac7d42f6ea --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx @@ -0,0 +1,50 @@ +import React, { useEffect } from 'react'; +import { DataSourceHttpSettings, DataSourcePluginOptionsEditorProps } from '@grafana/ui'; +import { ElasticsearchOptions } from '../types'; +import { defaultMaxConcurrentShardRequests, ElasticDetails } from './ElasticDetails'; +import { LogsConfig } from './LogsConfig'; + +export type Props = DataSourcePluginOptionsEditorProps; +export const ConfigEditor = (props: Props) => { + const { options, onOptionsChange } = props; + + // Apply some defaults on initial render + useEffect(() => { + const esVersion = options.jsonData.esVersion || 5; + onOptionsChange({ + ...options, + jsonData: { + ...options.jsonData, + timeField: options.jsonData.timeField || '@timestamp', + esVersion, + maxConcurrentShardRequests: + options.jsonData.maxConcurrentShardRequests || defaultMaxConcurrentShardRequests(esVersion), + logMessageField: options.jsonData.logMessageField || '', + logLevelField: options.jsonData.logLevelField || '', + }, + }); + }, []); + + return ( + <> + + + + + + onOptionsChange({ + ...options, + jsonData: newValue, + }) + } + /> + + ); +}; diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx new file mode 100644 index 00000000000..1dcc1347228 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { last } from 'lodash'; +import { mount } from 'enzyme'; +import { ElasticDetails } from './ElasticDetails'; +import { createDefaultConfigOptions } from './mocks'; +import { Select } from '@grafana/ui'; + +describe('ElasticDetails', () => { + it('should render without error', () => { + mount( {}} value={createDefaultConfigOptions()} />); + }); + + it('should render "Max concurrent Shard Requests" if version high enough', () => { + const wrapper = mount( {}} value={createDefaultConfigOptions()} />); + expect(wrapper.find('input[aria-label="Max concurrent Shard Requests input"]').length).toBe(1); + }); + + it('should not render "Max concurrent Shard Requests" if version is low', () => { + const options = createDefaultConfigOptions(); + options.jsonData.esVersion = 5; + const wrapper = mount( {}} value={options} />); + expect(wrapper.find('input[aria-label="Max concurrent Shard Requests input"]').length).toBe(0); + }); + + it('should change database on interval change when not set explicitly', () => { + const onChangeMock = jest.fn(); + const wrapper = mount(); + const selectEl = wrapper.find({ label: 'Pattern' }).find(Select); + selectEl.props().onChange({ value: 'Daily', label: 'Daily' }); + + expect(onChangeMock.mock.calls[0][0].jsonData.interval).toBe('Daily'); + expect(onChangeMock.mock.calls[0][0].database).toBe('[logstash-]YYYY.MM.DD'); + }); + + it('should change database on interval change if pattern is from example', () => { + const onChangeMock = jest.fn(); + const options = createDefaultConfigOptions(); + options.database = '[logstash-]YYYY.MM.DD.HH'; + const wrapper = mount(); + + const selectEl = wrapper.find({ label: 'Pattern' }).find(Select); + selectEl.props().onChange({ value: 'Monthly', label: 'Monthly' }); + + expect(onChangeMock.mock.calls[0][0].jsonData.interval).toBe('Monthly'); + expect(onChangeMock.mock.calls[0][0].database).toBe('[logstash-]YYYY.MM'); + }); + + describe('version change', () => { + const testCases = [ + { version: 50, expectedMaxConcurrentShardRequests: 256 }, + { version: 50, maxConcurrentShardRequests: 50, expectedMaxConcurrentShardRequests: 50 }, + { version: 56, expectedMaxConcurrentShardRequests: 256 }, + { version: 56, maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 256 }, + { version: 56, maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 256 }, + { version: 56, maxConcurrentShardRequests: 200, expectedMaxConcurrentShardRequests: 200 }, + { version: 70, expectedMaxConcurrentShardRequests: 5 }, + { version: 70, maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 5 }, + { version: 70, maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 5 }, + { version: 70, maxConcurrentShardRequests: 6, expectedMaxConcurrentShardRequests: 6 }, + ]; + + const onChangeMock = jest.fn(); + const options = createDefaultConfigOptions(); + const wrapper = mount(); + + testCases.forEach(tc => { + it(`sets maxConcurrentShardRequests = ${tc.maxConcurrentShardRequests} if version = ${tc.version},`, () => { + wrapper.setProps({ + onChange: onChangeMock, + value: { + ...options, + jsonData: { + ...options.jsonData, + maxConcurrentShardRequests: tc.maxConcurrentShardRequests, + }, + }, + }); + + const selectEl = wrapper.find({ label: 'Version' }).find(Select); + selectEl.props().onChange({ value: tc.version, label: tc.version.toString() }); + + expect(last(onChangeMock.mock.calls)[0].jsonData.maxConcurrentShardRequests).toBe( + tc.expectedMaxConcurrentShardRequests + ); + }); + }); + }); +}); diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx new file mode 100644 index 00000000000..1c3852dfa32 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx @@ -0,0 +1,221 @@ +import React from 'react'; +import { DataSourceSettings, EventsWithValidation, FormField, Input, regexValidation, Select } from '@grafana/ui'; +import { ElasticsearchOptions } from '../types'; +import { SelectableValue } from '@grafana/data'; + +const indexPatternTypes = [ + { label: 'No pattern', value: 'none' }, + { label: 'Hourly', value: 'Hourly', example: '[logstash-]YYYY.MM.DD.HH' }, + { label: 'Daily', value: 'Daily', example: '[logstash-]YYYY.MM.DD' }, + { label: 'Weekly', value: 'Weekly', example: '[logstash-]GGGG.WW' }, + { label: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM' }, + { label: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY' }, +]; + +const esVersions = [ + { label: '2.x', value: 2 }, + { label: '5.x', value: 5 }, + { label: '5.6+', value: 56 }, + { label: '6.0+', value: 60 }, + { label: '7.0+', value: 70 }, +]; + +type Props = { + value: DataSourceSettings; + onChange: (value: DataSourceSettings) => void; +}; +export const ElasticDetails = (props: Props) => { + const { value, onChange } = props; + + return ( + <> +

Elasticsearch details

+ +
+
+
+ +
+ +
+ + pattern.value === (value.jsonData.interval === undefined ? 'none' : value.jsonData.interval) + )} + /> + } + /> +
+
+ +
+ +
+ +
+ + { + const maxConcurrentShardRequests = getMaxConcurrenShardRequestOrDefault( + value.jsonData.maxConcurrentShardRequests, + option.value + ); + onChange({ + ...value, + jsonData: { + ...value.jsonData, + esVersion: option.value, + maxConcurrentShardRequests, + }, + }); + }} + value={esVersions.find(version => version.value === value.jsonData.esVersion)} + /> + } + /> + +
+ {value.jsonData.esVersion >= 56 && ( +
+ +
+ )} +
+
+ + } + tooltip={ + <> + A lower limit for the auto group by time interval. Recommended to be set to write frequency, for + example 1m if your data is written every minute. + + } + /> +
+
+
+ + ); +}; + +const changeHandler = ( + key: keyof DataSourceSettings, + value: Props['value'], + onChange: Props['onChange'] +) => (event: React.SyntheticEvent) => { + onChange({ + ...value, + [key]: event.currentTarget.value, + }); +}; + +const jsonDataChangeHandler = (key: keyof ElasticsearchOptions, value: Props['value'], onChange: Props['onChange']) => ( + event: React.SyntheticEvent +) => { + onChange({ + ...value, + jsonData: { + ...value.jsonData, + [key]: event.currentTarget.value, + }, + }); +}; + +const intervalHandler = (value: Props['value'], onChange: Props['onChange']) => (option: SelectableValue) => { + const { database } = value; + // If option value is undefined it will send its label instead so we have to convert made up value to undefined here. + const newInterval = option.value === 'none' ? undefined : option.value; + + if (!database || database.length === 0 || database.startsWith('[logstash-]')) { + let newDatabase = ''; + if (newInterval !== undefined) { + const pattern = indexPatternTypes.find(pattern => pattern.value === newInterval); + if (pattern) { + newDatabase = pattern.example; + } + } + + onChange({ + ...value, + database: newDatabase, + jsonData: { + ...value.jsonData, + interval: newInterval, + }, + }); + } else { + onChange({ + ...value, + jsonData: { + ...value.jsonData, + interval: newInterval, + }, + }); + } +}; + +function getMaxConcurrenShardRequestOrDefault(maxConcurrentShardRequests: number, version: number): number { + if (maxConcurrentShardRequests === 5 && version < 70) { + return 256; + } + + if (maxConcurrentShardRequests === 256 && version >= 70) { + return 5; + } + + return maxConcurrentShardRequests || defaultMaxConcurrentShardRequests(version); +} + +export function defaultMaxConcurrentShardRequests(version: number) { + return version >= 70 ? 5 : 256; +} diff --git a/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.test.tsx b/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.test.tsx new file mode 100644 index 00000000000..5ef0778867c --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.test.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { LogsConfig } from './LogsConfig'; +import { createDefaultConfigOptions } from './mocks'; +import { FormField } from '@grafana/ui'; + +describe('ElasticDetails', () => { + it('should render without error', () => { + mount( {}} value={createDefaultConfigOptions().jsonData} />); + }); + + it('should render fields', () => { + const wrapper = shallow( {}} value={createDefaultConfigOptions().jsonData} />); + expect(wrapper.find(FormField).length).toBe(2); + }); + + it('should pass correct data to onChange', () => { + const onChangeMock = jest.fn(); + const wrapper = mount(); + const inputEl = wrapper + .find(FormField) + .at(0) + .find('input'); + (inputEl.getDOMNode() as any).value = 'test_field'; + inputEl.simulate('change'); + expect(onChangeMock.mock.calls[0][0].logMessageField).toBe('test_field'); + }); +}); diff --git a/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.tsx b/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.tsx new file mode 100644 index 00000000000..b1c98825b7f --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { FormField } from '@grafana/ui'; +import { ElasticsearchOptions } from '../types'; + +type Props = { + value: ElasticsearchOptions; + onChange: (value: ElasticsearchOptions) => void; +}; +export const LogsConfig = (props: Props) => { + const { value, onChange } = props; + const changeHandler = (key: keyof ElasticsearchOptions) => ( + event: React.SyntheticEvent + ) => { + onChange({ + ...value, + [key]: event.currentTarget.value, + }); + }; + + return ( + <> +

Logs

+ +
+
+ +
+
+ +
+
+ + ); +}; diff --git a/public/app/plugins/datasource/elasticsearch/configuration/mocks.ts b/public/app/plugins/datasource/elasticsearch/configuration/mocks.ts new file mode 100644 index 00000000000..c52a749f5a7 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/configuration/mocks.ts @@ -0,0 +1,15 @@ +import { DataSourceSettings } from '@grafana/ui'; +import { ElasticsearchOptions } from '../types'; +import { createDatasourceSettings } from '../../../../features/datasources/mocks'; + +export function createDefaultConfigOptions(): DataSourceSettings { + return createDatasourceSettings({ + timeField: '@time', + esVersion: 70, + interval: 'Hourly', + timeInterval: '10s', + maxConcurrentShardRequests: 300, + logMessageField: 'test.message', + logLevelField: 'test.level', + }); +} diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 191d9b6824b..bc1fd4fdaee 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -583,16 +583,3 @@ export class ElasticDatasource extends DataSourceApi= 70) { - return 5; - } - - const defaultMaxConcurrentShardRequests = options.esVersion >= 70 ? 5 : 256; - return options.maxConcurrentShardRequests || defaultMaxConcurrentShardRequests; -} diff --git a/public/app/plugins/datasource/elasticsearch/module.ts b/public/app/plugins/datasource/elasticsearch/module.ts index ea384fec5b9..b8ef8048087 100644 --- a/public/app/plugins/datasource/elasticsearch/module.ts +++ b/public/app/plugins/datasource/elasticsearch/module.ts @@ -1,8 +1,8 @@ import { DataSourcePlugin } from '@grafana/ui'; import { ElasticDatasource } from './datasource'; import { ElasticQueryCtrl } from './query_ctrl'; -import { ElasticConfigCtrl } from './config_ctrl'; import ElasticsearchQueryField from './components/ElasticsearchQueryField'; +import { ConfigEditor } from './configuration/ConfigEditor'; class ElasticAnnotationsQueryCtrl { static templateUrl = 'partials/annotations.editor.html'; @@ -10,6 +10,6 @@ class ElasticAnnotationsQueryCtrl { export const plugin = new DataSourcePlugin(ElasticDatasource) .setQueryCtrl(ElasticQueryCtrl) - .setConfigCtrl(ElasticConfigCtrl) + .setConfigEditor(ConfigEditor) .setExploreLogsQueryField(ElasticsearchQueryField) .setAnnotationQueryCtrl(ElasticAnnotationsQueryCtrl); diff --git a/public/app/plugins/datasource/elasticsearch/partials/config.html b/public/app/plugins/datasource/elasticsearch/partials/config.html deleted file mode 100644 index d959ee97ac2..00000000000 --- a/public/app/plugins/datasource/elasticsearch/partials/config.html +++ /dev/null @@ -1,66 +0,0 @@ - - - -

Elasticsearch details

- -
-
-
- Index name - -
- -
- Pattern - - - -
-
- -
- Time field name - -
- -
- Version - - - -
-
- Max concurrent Shard Requests - -
-
-
- Min time interval - - - A lower limit for the auto group by time interval. Recommended to be set to write frequency, - for example 1m if your data is written every minute. - -
-
-
- -Logs - -
-
- Message field name - -
-
- Level field name - -
-
diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts index 005f490b9a6..0345875944d 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts @@ -1,7 +1,7 @@ import angular, { IQService } from 'angular'; import { dateMath } from '@grafana/data'; import _ from 'lodash'; -import { ElasticDatasource, getMaxConcurrenShardRequestOrDefault } from '../datasource'; +import { ElasticDatasource } from '../datasource'; import { toUtc, dateTime } from '@grafana/data'; import { BackendSrv } from 'app/core/services/backend_srv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; @@ -646,27 +646,3 @@ describe('ElasticDatasource', function(this: any) { }); }); }); - -describe('getMaxConcurrenShardRequestOrDefault', () => { - const testCases = [ - { version: 50, expectedMaxConcurrentShardRequests: 256 }, - { version: 50, maxConcurrentShardRequests: 50, expectedMaxConcurrentShardRequests: 50 }, - { version: 56, expectedMaxConcurrentShardRequests: 256 }, - { version: 56, maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 256 }, - { version: 56, maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 256 }, - { version: 56, maxConcurrentShardRequests: 200, expectedMaxConcurrentShardRequests: 200 }, - { version: 70, expectedMaxConcurrentShardRequests: 5 }, - { version: 70, maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 5 }, - { version: 70, maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 5 }, - { version: 70, maxConcurrentShardRequests: 6, expectedMaxConcurrentShardRequests: 6 }, - ]; - - testCases.forEach(tc => { - it(`version = ${tc.version}, maxConcurrentShardRequests = ${tc.maxConcurrentShardRequests}`, () => { - const options = { esVersion: tc.version, maxConcurrentShardRequests: tc.maxConcurrentShardRequests }; - expect(getMaxConcurrenShardRequestOrDefault(options as ElasticsearchOptions)).toBe( - tc.expectedMaxConcurrentShardRequests - ); - }); - }); -}); diff --git a/public/app/plugins/datasource/input/InputConfigEditor.tsx b/public/app/plugins/datasource/input/InputConfigEditor.tsx index f99643e0525..51cc1fefbc6 100644 --- a/public/app/plugins/datasource/input/InputConfigEditor.tsx +++ b/public/app/plugins/datasource/input/InputConfigEditor.tsx @@ -4,13 +4,11 @@ import React, { PureComponent } from 'react'; // Types import { InputOptions } from './types'; -import { DataSourcePluginOptionsEditorProps, DataSourceSettings, TableInputCSV } from '@grafana/ui'; +import { DataSourcePluginOptionsEditorProps, TableInputCSV } from '@grafana/ui'; import { DataFrame, MutableDataFrame } from '@grafana/data'; import { dataFrameToCSV } from './utils'; -type InputSettings = DataSourceSettings; - -interface Props extends DataSourcePluginOptionsEditorProps {} +interface Props extends DataSourcePluginOptionsEditorProps {} interface State { text: string; diff --git a/public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx b/public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx index 07116dff36e..030da96e5ad 100644 --- a/public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/AnnotationsQueryEditor.tsx @@ -2,14 +2,15 @@ import React, { memo } from 'react'; // Types -import { DataSourceApi, DataSourceJsonData, DataSourceStatus } from '@grafana/ui'; +import { DataSourceStatus } from '@grafana/ui'; import { LokiQuery } from '../types'; import { useLokiSyntax } from './useLokiSyntax'; import { LokiQueryFieldForm } from './LokiQueryFieldForm'; +import LokiDatasource from '../datasource'; interface Props { expr: string; - datasource: DataSourceApi; + datasource: LokiDatasource; onChange: (expr: string) => void; } diff --git a/public/app/plugins/datasource/loki/components/ConfigEditor.test.tsx b/public/app/plugins/datasource/loki/components/ConfigEditor.test.tsx new file mode 100644 index 00000000000..697c90b8a07 --- /dev/null +++ b/public/app/plugins/datasource/loki/components/ConfigEditor.test.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { ConfigEditor } from './ConfigEditor'; +import { createDefaultConfigOptions } from '../mocks'; +import { DataSourceHttpSettings } from '@grafana/ui'; + +describe('ConfigEditor', () => { + it('should render without error', () => { + mount( {}} options={createDefaultConfigOptions()} />); + }); + + it('should render the right sections', () => { + const wrapper = mount( {}} options={createDefaultConfigOptions()} />); + expect(wrapper.find(DataSourceHttpSettings).length).toBe(1); + expect(wrapper.find({ label: 'Maximum lines' }).length).toBe(1); + }); + + it('should pass correct data to onChange', () => { + const onChangeMock = jest.fn(); + const wrapper = mount(); + const inputWrapper = wrapper.find({ label: 'Maximum lines' }).find('input'); + (inputWrapper.getDOMNode() as any).value = 42; + inputWrapper.simulate('change'); + expect(onChangeMock.mock.calls[0][0].jsonData.maxLines).toBe('42'); + }); +}); diff --git a/public/app/plugins/datasource/loki/components/ConfigEditor.tsx b/public/app/plugins/datasource/loki/components/ConfigEditor.tsx new file mode 100644 index 00000000000..6306cc4c9e7 --- /dev/null +++ b/public/app/plugins/datasource/loki/components/ConfigEditor.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { DataSourceHttpSettings, DataSourcePluginOptionsEditorProps, DataSourceSettings, FormField } from '@grafana/ui'; +import { LokiOptions } from '../types'; + +export type Props = DataSourcePluginOptionsEditorProps; + +const makeJsonUpdater = (field: keyof LokiOptions) => ( + options: DataSourceSettings, + value: T +): DataSourceSettings => { + return { + ...options, + jsonData: { + ...options.jsonData, + [field]: value, + }, + }; +}; + +const setMaxLines = makeJsonUpdater('maxLines'); + +export const ConfigEditor = (props: Props) => { + const { options, onOptionsChange } = props; + + return ( + <> + + +
+
+
+ onOptionsChange(setMaxLines(options, value))} + /> +
+
+
+ + ); +}; + +type MaxLinesFieldProps = { + value: string; + onChange: (value: string) => void; +}; + +const MaxLinesField = (props: MaxLinesFieldProps) => { + const { value, onChange } = props; + return ( + onChange(event.currentTarget.value)} + spellCheck={false} + placeholder="1000" + /> + } + tooltip={ + <> + Loki queries must contain a limit of the maximum number of lines returned (default: 1000). Increase this limit + to have a bigger result set for ad-hoc analysis. Decrease this limit if your browser becomes sluggish when + displaying the log results. + + } + /> + ); +}; diff --git a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx index ae1835bb4ec..2c999c9648c 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx @@ -15,11 +15,12 @@ import { Plugin, Node } from 'slate'; // Types import { LokiQuery } from '../types'; import { TypeaheadOutput } from 'app/types/explore'; -import { DataSourceApi, ExploreQueryFieldProps, DataSourceStatus, DOMUtil } from '@grafana/ui'; +import { ExploreQueryFieldProps, DataSourceStatus, DOMUtil } from '@grafana/ui'; import { AbsoluteTimeRange } from '@grafana/data'; import { Grammar } from 'prismjs'; import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider'; import { SuggestionsState } from 'app/features/explore/slate-plugins/suggestions'; +import LokiDatasource from '../datasource'; function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) { if (datasourceStatus === DataSourceStatus.Disconnected) { @@ -68,7 +69,7 @@ export interface CascaderOption { disabled?: boolean; } -export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps, LokiQuery> { +export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps { history: LokiHistoryItem[]; syntax: Grammar; logLabelOptions: any[]; diff --git a/public/app/plugins/datasource/loki/mocks.ts b/public/app/plugins/datasource/loki/mocks.ts index 7e91c51c105..84e75244f80 100644 --- a/public/app/plugins/datasource/loki/mocks.ts +++ b/public/app/plugins/datasource/loki/mocks.ts @@ -1,4 +1,7 @@ import LokiDatasource from './datasource'; +import { DataSourceSettings } from '@grafana/ui'; +import { LokiOptions } from './types'; +import { createDatasourceSettings } from '../../../features/datasources/mocks'; export function makeMockLokiDatasource(labelsAndValues: { [label: string]: string[] }): LokiDatasource { const labels = Object.keys(labelsAndValues); @@ -25,3 +28,9 @@ export function makeMockLokiDatasource(labelsAndValues: { [label: string]: strin }, } as any; } + +export function createDefaultConfigOptions(): DataSourceSettings { + return createDatasourceSettings({ + maxLines: '531', + }); +} diff --git a/public/app/plugins/datasource/loki/module.ts b/public/app/plugins/datasource/loki/module.ts index 652a596e371..a2cd2bcc589 100644 --- a/public/app/plugins/datasource/loki/module.ts +++ b/public/app/plugins/datasource/loki/module.ts @@ -1,19 +1,15 @@ +import { DataSourcePlugin } from '@grafana/ui'; import Datasource from './datasource'; import LokiCheatSheet from './components/LokiCheatSheet'; import LokiQueryField from './components/LokiQueryField'; import LokiQueryEditor from './components/LokiQueryEditor'; import { LokiAnnotationsQueryCtrl } from './LokiAnnotationsQueryCtrl'; +import { ConfigEditor } from './components/ConfigEditor'; -export class LokiConfigCtrl { - static templateUrl = 'partials/config.html'; -} - -export { - Datasource, - LokiQueryEditor as QueryEditor, - LokiConfigCtrl as ConfigCtrl, - LokiQueryField as ExploreQueryField, - LokiCheatSheet as ExploreStartPage, - LokiAnnotationsQueryCtrl as AnnotationsQueryCtrl, -}; +export const plugin = new DataSourcePlugin(Datasource) + .setQueryEditor(LokiQueryEditor) + .setConfigEditor(ConfigEditor) + .setExploreQueryField(LokiQueryField) + .setExploreStartPage(LokiCheatSheet) + .setAnnotationQueryCtrl(LokiAnnotationsQueryCtrl); diff --git a/public/app/plugins/datasource/loki/partials/config.html b/public/app/plugins/datasource/loki/partials/config.html deleted file mode 100644 index 425e460a240..00000000000 --- a/public/app/plugins/datasource/loki/partials/config.html +++ /dev/null @@ -1,16 +0,0 @@ - - - -
-
-
- Maximum lines - - - Loki queries must contain a limit of the maximum number of lines returned (default: 1000). - Increase this limit to have a bigger result set for ad-hoc analysis. - Decrease this limit if your browser becomes sluggish when displaying the log results. - -
-
-
diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index e8690c507e6..63ef0ad03d0 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -1,5 +1,5 @@ import { Unsubscribable } from 'rxjs'; -import { ComponentClass } from 'react'; +import { ComponentType } from 'react'; import { DataQuery, DataSourceSelectItem, @@ -148,7 +148,7 @@ export interface ExploreItemState { /** * React component to be shown when no queries have been run yet, e.g., for a query language cheat sheet. */ - StartPage?: ComponentClass; + StartPage?: ComponentType; /** * Width used for calculating the graph interval (can't have more datapoints than pixels) */