mirror of https://github.com/grafana/grafana
ReactMigration: Migrate Loki and Elastic config pages to React (#19979)
parent
771f21ed09
commit
551e24f9f8
@ -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, |
||||
}; |
||||
}; |
@ -0,0 +1,23 @@ |
||||
import { DataSourceSettings } from '@grafana/ui'; |
||||
|
||||
export function createDatasourceSettings<T>(jsonData: T): DataSourceSettings<T> { |
||||
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, |
||||
}; |
||||
} |
@ -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<ElasticsearchOptions>; |
||||
|
||||
/** @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); |
||||
} |
||||
} |
@ -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(<ConfigEditor onOptionsChange={() => {}} options={createDefaultConfigOptions()} />); |
||||
}); |
||||
|
||||
it('should render all parts of the config', () => { |
||||
const wrapper = shallow(<ConfigEditor onOptionsChange={() => {}} 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( |
||||
<ConfigEditor |
||||
onOptionsChange={options => { |
||||
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( |
||||
<ConfigEditor |
||||
onOptionsChange={options => { |
||||
expect(options.jsonData.esVersion).toBe(70); |
||||
expect(options.jsonData.timeField).toBe('@time'); |
||||
expect(options.jsonData.maxConcurrentShardRequests).toBe(300); |
||||
}} |
||||
options={createDefaultConfigOptions()} |
||||
/> |
||||
); |
||||
}); |
||||
}); |
@ -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<ElasticsearchOptions>; |
||||
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 ( |
||||
<> |
||||
<DataSourceHttpSettings |
||||
defaultUrl={'http://localhost:3100'} |
||||
dataSourceConfig={options} |
||||
showAccessOptions={true} |
||||
onChange={onOptionsChange} |
||||
/> |
||||
|
||||
<ElasticDetails value={options} onChange={onOptionsChange} /> |
||||
|
||||
<LogsConfig |
||||
value={options.jsonData} |
||||
onChange={newValue => |
||||
onOptionsChange({ |
||||
...options, |
||||
jsonData: newValue, |
||||
}) |
||||
} |
||||
/> |
||||
</> |
||||
); |
||||
}; |
@ -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(<ElasticDetails onChange={() => {}} value={createDefaultConfigOptions()} />); |
||||
}); |
||||
|
||||
it('should render "Max concurrent Shard Requests" if version high enough', () => { |
||||
const wrapper = mount(<ElasticDetails onChange={() => {}} 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(<ElasticDetails onChange={() => {}} 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(<ElasticDetails onChange={onChangeMock} value={createDefaultConfigOptions()} />); |
||||
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(<ElasticDetails onChange={onChangeMock} value={options} />); |
||||
|
||||
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(<ElasticDetails onChange={onChangeMock} value={options} />); |
||||
|
||||
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 |
||||
); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -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<ElasticsearchOptions>; |
||||
onChange: (value: DataSourceSettings<ElasticsearchOptions>) => void; |
||||
}; |
||||
export const ElasticDetails = (props: Props) => { |
||||
const { value, onChange } = props; |
||||
|
||||
return ( |
||||
<> |
||||
<h3 className="page-heading">Elasticsearch details</h3> |
||||
|
||||
<div className="gf-form-group"> |
||||
<div className="gf-form-inline"> |
||||
<div className="gf-form max-width-25"> |
||||
<FormField |
||||
labelWidth={10} |
||||
inputWidth={15} |
||||
label="Index name" |
||||
value={value.database || ''} |
||||
onChange={changeHandler('database', value, onChange)} |
||||
placeholder={'es-index-name'} |
||||
required |
||||
/> |
||||
</div> |
||||
|
||||
<div className="gf-form width-14"> |
||||
<FormField |
||||
labelWidth={10} |
||||
label="Pattern" |
||||
inputEl={ |
||||
<Select |
||||
options={indexPatternTypes} |
||||
onChange={intervalHandler(value, onChange)} |
||||
value={indexPatternTypes.find( |
||||
pattern => |
||||
pattern.value === (value.jsonData.interval === undefined ? 'none' : value.jsonData.interval) |
||||
)} |
||||
/> |
||||
} |
||||
/> |
||||
</div> |
||||
</div> |
||||
|
||||
<div className="gf-form max-width-25"> |
||||
<FormField |
||||
labelWidth={10} |
||||
inputWidth={15} |
||||
label="Time field name" |
||||
value={value.jsonData.timeField || ''} |
||||
onChange={jsonDataChangeHandler('timeField', value, onChange)} |
||||
required |
||||
/> |
||||
</div> |
||||
|
||||
<div className="gf-form"> |
||||
<span className="gf-form-select-wrapper"> |
||||
<FormField |
||||
labelWidth={10} |
||||
label="Version" |
||||
inputEl={ |
||||
<Select |
||||
options={esVersions} |
||||
onChange={option => { |
||||
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)} |
||||
/> |
||||
} |
||||
/> |
||||
</span> |
||||
</div> |
||||
{value.jsonData.esVersion >= 56 && ( |
||||
<div className="gf-form max-width-30"> |
||||
<FormField |
||||
aria-label={'Max concurrent Shard Requests input'} |
||||
labelWidth={15} |
||||
label="Max concurrent Shard Requests" |
||||
value={value.jsonData.maxConcurrentShardRequests || ''} |
||||
onChange={jsonDataChangeHandler('maxConcurrentShardRequests', value, onChange)} |
||||
/> |
||||
</div> |
||||
)} |
||||
<div className="gf-form-inline"> |
||||
<div className="gf-form"> |
||||
<FormField |
||||
labelWidth={10} |
||||
label="Min time interval" |
||||
inputEl={ |
||||
<Input |
||||
className={'width-6'} |
||||
value={value.jsonData.timeInterval || ''} |
||||
onChange={jsonDataChangeHandler('timeInterval', value, onChange)} |
||||
placeholder="10s" |
||||
validationEvents={{ |
||||
[EventsWithValidation.onBlur]: [ |
||||
regexValidation( |
||||
/^\d+(ms|[Mwdhmsy])$/, |
||||
'Value is not valid, you can use number with time unit specifier: y, M, w, d, h, m, s' |
||||
), |
||||
], |
||||
}} |
||||
/> |
||||
} |
||||
tooltip={ |
||||
<> |
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for |
||||
example <code>1m</code> if your data is written every minute. |
||||
</> |
||||
} |
||||
/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
const changeHandler = ( |
||||
key: keyof DataSourceSettings<ElasticsearchOptions>, |
||||
value: Props['value'], |
||||
onChange: Props['onChange'] |
||||
) => (event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement>) => { |
||||
onChange({ |
||||
...value, |
||||
[key]: event.currentTarget.value, |
||||
}); |
||||
}; |
||||
|
||||
const jsonDataChangeHandler = (key: keyof ElasticsearchOptions, value: Props['value'], onChange: Props['onChange']) => ( |
||||
event: React.SyntheticEvent<HTMLInputElement | HTMLSelectElement> |
||||
) => { |
||||
onChange({ |
||||
...value, |
||||
jsonData: { |
||||
...value.jsonData, |
||||
[key]: event.currentTarget.value, |
||||
}, |
||||
}); |
||||
}; |
||||
|
||||
const intervalHandler = (value: Props['value'], onChange: Props['onChange']) => (option: SelectableValue<string>) => { |
||||
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; |
||||
} |
@ -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(<LogsConfig onChange={() => {}} value={createDefaultConfigOptions().jsonData} />); |
||||
}); |
||||
|
||||
it('should render fields', () => { |
||||
const wrapper = shallow(<LogsConfig onChange={() => {}} value={createDefaultConfigOptions().jsonData} />); |
||||
expect(wrapper.find(FormField).length).toBe(2); |
||||
}); |
||||
|
||||
it('should pass correct data to onChange', () => { |
||||
const onChangeMock = jest.fn(); |
||||
const wrapper = mount(<LogsConfig onChange={onChangeMock} value={createDefaultConfigOptions().jsonData} />); |
||||
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'); |
||||
}); |
||||
}); |
@ -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<HTMLInputElement | HTMLSelectElement> |
||||
) => { |
||||
onChange({ |
||||
...value, |
||||
[key]: event.currentTarget.value, |
||||
}); |
||||
}; |
||||
|
||||
return ( |
||||
<> |
||||
<h3 className="page-heading">Logs</h3> |
||||
|
||||
<div className="gf-form-group"> |
||||
<div className="gf-form max-width-30"> |
||||
<FormField |
||||
labelWidth={11} |
||||
label="Message field name" |
||||
value={value.logMessageField} |
||||
onChange={changeHandler('logMessageField')} |
||||
placeholder="_source" |
||||
/> |
||||
</div> |
||||
<div className="gf-form max-width-30"> |
||||
<FormField |
||||
labelWidth={11} |
||||
label="Level field name" |
||||
value={value.logLevelField} |
||||
onChange={changeHandler('logLevelField')} |
||||
/> |
||||
</div> |
||||
</div> |
||||
</> |
||||
); |
||||
}; |
@ -0,0 +1,15 @@ |
||||
import { DataSourceSettings } from '@grafana/ui'; |
||||
import { ElasticsearchOptions } from '../types'; |
||||
import { createDatasourceSettings } from '../../../../features/datasources/mocks'; |
||||
|
||||
export function createDefaultConfigOptions(): DataSourceSettings<ElasticsearchOptions> { |
||||
return createDatasourceSettings<ElasticsearchOptions>({ |
||||
timeField: '@time', |
||||
esVersion: 70, |
||||
interval: 'Hourly', |
||||
timeInterval: '10s', |
||||
maxConcurrentShardRequests: 300, |
||||
logMessageField: 'test.message', |
||||
logLevelField: 'test.level', |
||||
}); |
||||
} |
@ -1,66 +0,0 @@ |
||||
<datasource-http-settings current="ctrl.current" suggest-url="http://localhost:9200"> |
||||
</datasource-http-settings> |
||||
|
||||
<h3 class="page-heading">Elasticsearch details</h3> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form max-width-25"> |
||||
<span class="gf-form-label width-9">Index name</span> |
||||
<input class="gf-form-input" type="text" ng-model='ctrl.current.database' placeholder="" required></input> |
||||
</div> |
||||
|
||||
<div class="gf-form width-14"> |
||||
<span class="gf-form-label width-9">Pattern</span> |
||||
<span class="gf-form-select-wrapper"> |
||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.current.jsonData.interval" ng-options="f.value as f.name for f in ctrl.indexPatternTypes" ng-change="ctrl.indexPatternTypeChanged()" ></select> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form max-width-25"> |
||||
<span class="gf-form-label width-9">Time field name</span> |
||||
<input class="gf-form-input" type="text" ng-model='ctrl.current.jsonData.timeField' placeholder="" required ng-init=""></input> |
||||
</div> |
||||
|
||||
<div class="gf-form"> |
||||
<span class="gf-form-label width-9">Version</span> |
||||
<span class="gf-form-select-wrapper"> |
||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.current.jsonData.esVersion" ng-options="f.value as f.name for f in ctrl.esVersions" ng-change="ctrl.versionChanged()"></select> |
||||
</span> |
||||
</div> |
||||
<div class="gf-form max-width-30" ng-if="ctrl.current.jsonData.esVersion>=56"> |
||||
<span class="gf-form-label width-15">Max concurrent Shard Requests</span> |
||||
<input class="gf-form-input" type="text" ng-model='ctrl.current.jsonData.maxConcurrentShardRequests' placeholder="" required></input> |
||||
</div> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<span class="gf-form-label width-9">Min time interval</span> |
||||
<input |
||||
type="text" |
||||
class="gf-form-input width-6 gf-form-input--has-help-icon" |
||||
ng-model="ctrl.current.jsonData.timeInterval" |
||||
spellcheck='false' |
||||
placeholder="10s" |
||||
ng-pattern="/^\d+(ms|[Mwdhmsy])$/" |
||||
></input> |
||||
<info-popover mode="right-absolute"> |
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency, |
||||
for example <code>1m</code> if your data is written every minute. |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<b>Logs</b> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-11">Message field name</span> |
||||
<input class="gf-form-input" type="text" ng-model='ctrl.current.jsonData.logMessageField' placeholder="_source" /> |
||||
</div> |
||||
<div class="gf-form max-width-30"> |
||||
<span class="gf-form-label width-11">Level field name</span> |
||||
<input class="gf-form-input" type="text" ng-model='ctrl.current.jsonData.logLevelField' placeholder="" /> |
||||
</div> |
||||
</div> |
@ -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(<ConfigEditor onOptionsChange={() => {}} options={createDefaultConfigOptions()} />); |
||||
}); |
||||
|
||||
it('should render the right sections', () => { |
||||
const wrapper = mount(<ConfigEditor onOptionsChange={() => {}} 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(<ConfigEditor onOptionsChange={onChangeMock} options={createDefaultConfigOptions()} />); |
||||
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'); |
||||
}); |
||||
}); |
@ -0,0 +1,79 @@ |
||||
import React from 'react'; |
||||
import { DataSourceHttpSettings, DataSourcePluginOptionsEditorProps, DataSourceSettings, FormField } from '@grafana/ui'; |
||||
import { LokiOptions } from '../types'; |
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps<LokiOptions>; |
||||
|
||||
const makeJsonUpdater = <T extends any>(field: keyof LokiOptions) => ( |
||||
options: DataSourceSettings<LokiOptions>, |
||||
value: T |
||||
): DataSourceSettings<LokiOptions> => { |
||||
return { |
||||
...options, |
||||
jsonData: { |
||||
...options.jsonData, |
||||
[field]: value, |
||||
}, |
||||
}; |
||||
}; |
||||
|
||||
const setMaxLines = makeJsonUpdater('maxLines'); |
||||
|
||||
export const ConfigEditor = (props: Props) => { |
||||
const { options, onOptionsChange } = props; |
||||
|
||||
return ( |
||||
<> |
||||
<DataSourceHttpSettings |
||||
defaultUrl={'http://localhost:3100'} |
||||
dataSourceConfig={options} |
||||
showAccessOptions={false} |
||||
onChange={onOptionsChange} |
||||
/> |
||||
|
||||
<div className="gf-form-group"> |
||||
<div className="gf-form-inline"> |
||||
<div className="gf-form"> |
||||
<MaxLinesField |
||||
value={options.jsonData.maxLines} |
||||
onChange={value => onOptionsChange(setMaxLines(options, value))} |
||||
/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
type MaxLinesFieldProps = { |
||||
value: string; |
||||
onChange: (value: string) => void; |
||||
}; |
||||
|
||||
const MaxLinesField = (props: MaxLinesFieldProps) => { |
||||
const { value, onChange } = props; |
||||
return ( |
||||
<FormField |
||||
label="Maximum lines" |
||||
labelWidth={11} |
||||
inputWidth={20} |
||||
inputEl={ |
||||
<input |
||||
type="number" |
||||
className="gf-form-input width-8 gf-form-input--has-help-icon" |
||||
value={value} |
||||
onChange={event => 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. |
||||
</> |
||||
} |
||||
/> |
||||
); |
||||
}; |
@ -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); |
||||
|
@ -1,16 +0,0 @@ |
||||
<datasource-http-settings current="ctrl.current" no-direct-access="true"> |
||||
</datasource-http-settings> |
||||
|
||||
<div class="gf-form-group"> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<span class="gf-form-label width-8">Maximum lines</span> |
||||
<input type="text" class="gf-form-input width-8 gf-form-input--has-help-icon" ng-model="ctrl.current.jsonData.maxLines" spellcheck='false' placeholder="1000"></input> |
||||
<info-popover mode="right-absolute"> |
||||
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. |
||||
</info-popover> |
||||
</div> |
||||
</div> |
||||
</div> |
Loading…
Reference in new issue