mirror of
https://github.com/grafana/grafana.git
synced 2025-01-26 08:16:59 -06:00
ReactMigration: Migrate Loki and Elastic config pages to React (#19979)
This commit is contained in:
parent
771f21ed09
commit
551e24f9f8
@ -1,4 +1,4 @@
|
|||||||
import { ComponentType, ComponentClass } from 'react';
|
import { ComponentType } from 'react';
|
||||||
import {
|
import {
|
||||||
TimeRange,
|
TimeRange,
|
||||||
RawTimeRange,
|
RawTimeRange,
|
||||||
@ -16,10 +16,9 @@ import { PluginMeta, GrafanaPlugin } from './plugin';
|
|||||||
import { PanelData } from './panel';
|
import { PanelData } from './panel';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
// NOTE: this seems more general than just DataSource
|
export interface DataSourcePluginOptionsEditorProps<JSONData = DataSourceJsonData, SecureJSONData = {}> {
|
||||||
export interface DataSourcePluginOptionsEditorProps<TOptions> {
|
options: DataSourceSettings<JSONData, SecureJSONData>;
|
||||||
options: TOptions;
|
onOptionsChange: (options: DataSourceSettings<JSONData, SecureJSONData>) => void;
|
||||||
onOptionsChange: (options: TOptions) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataSourcePlugin<
|
export class DataSourcePlugin<
|
||||||
@ -36,7 +35,7 @@ export class DataSourcePlugin<
|
|||||||
this.components = {};
|
this.components = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfigEditor(editor: ComponentType<DataSourcePluginOptionsEditorProps<DataSourceSettings<TOptions>>>) {
|
setConfigEditor(editor: ComponentType<DataSourcePluginOptionsEditorProps<TOptions>>) {
|
||||||
this.components.ConfigEditor = editor;
|
this.components.ConfigEditor = editor;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -61,22 +60,22 @@ export class DataSourcePlugin<
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
setExploreQueryField(ExploreQueryField: ComponentClass<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) {
|
setExploreQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) {
|
||||||
this.components.ExploreQueryField = ExploreQueryField;
|
this.components.ExploreQueryField = ExploreQueryField;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
setExploreMetricsQueryField(ExploreQueryField: ComponentClass<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) {
|
setExploreMetricsQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) {
|
||||||
this.components.ExploreMetricsQueryField = ExploreQueryField;
|
this.components.ExploreMetricsQueryField = ExploreQueryField;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
setExploreLogsQueryField(ExploreQueryField: ComponentClass<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) {
|
setExploreLogsQueryField(ExploreQueryField: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>) {
|
||||||
this.components.ExploreLogsQueryField = ExploreQueryField;
|
this.components.ExploreLogsQueryField = ExploreQueryField;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
setExploreStartPage(ExploreStartPage: ComponentClass<ExploreStartPageProps>) {
|
setExploreStartPage(ExploreStartPage: ComponentType<ExploreStartPageProps>) {
|
||||||
this.components.ExploreStartPage = ExploreStartPage;
|
this.components.ExploreStartPage = ExploreStartPage;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -134,11 +133,11 @@ export interface DataSourcePluginComponents<
|
|||||||
AnnotationsQueryCtrl?: any;
|
AnnotationsQueryCtrl?: any;
|
||||||
VariableQueryEditor?: any;
|
VariableQueryEditor?: any;
|
||||||
QueryEditor?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>;
|
QueryEditor?: ComponentType<QueryEditorProps<DSType, TQuery, TOptions>>;
|
||||||
ExploreQueryField?: ComponentClass<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
|
ExploreQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
|
||||||
ExploreMetricsQueryField?: ComponentClass<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
|
ExploreMetricsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
|
||||||
ExploreLogsQueryField?: ComponentClass<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
|
ExploreLogsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
|
||||||
ExploreStartPage?: ComponentClass<ExploreStartPageProps>;
|
ExploreStartPage?: ComponentType<ExploreStartPageProps>;
|
||||||
ConfigEditor?: ComponentType<DataSourcePluginOptionsEditorProps<DataSourceSettings<TOptions>>>;
|
ConfigEditor?: ComponentType<DataSourcePluginOptionsEditorProps<TOptions>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only exported for tests
|
// Only exported for tests
|
||||||
@ -508,6 +507,7 @@ export interface DataSourceSettings<T extends DataSourceJsonData = DataSourceJso
|
|||||||
secureJsonFields?: KeyValue<boolean>;
|
secureJsonFields?: KeyValue<boolean>;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
withCredentials: boolean;
|
withCredentials: boolean;
|
||||||
|
version?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
56
packages/grafana-ui/src/utils/validate.test.ts
Normal file
56
packages/grafana-ui/src/utils/validate.test.ts
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
};
|
@ -22,3 +22,12 @@ export const validate = (value: string, validationRules: ValidationRule[]) => {
|
|||||||
export const hasValidationEvent = (event: EventsWithValidation, validationEvents: ValidationEvents | undefined) => {
|
export const hasValidationEvent = (event: EventsWithValidation, validationEvents: ValidationEvents | undefined) => {
|
||||||
return validationEvents && validationEvents[event];
|
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',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
23
public/app/features/datasources/mocks.ts
Normal file
23
public/app/features/datasources/mocks.ts
Normal file
@ -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,5 +1,5 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { ComponentClass } from 'react';
|
import React, { ComponentType } from 'react';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
@ -63,7 +63,7 @@ const getStyles = memoizeOne(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface ExploreProps {
|
interface ExploreProps {
|
||||||
StartPage?: ComponentClass<ExploreStartPageProps>;
|
StartPage?: ComponentType<ExploreStartPageProps>;
|
||||||
changeSize: typeof changeSize;
|
changeSize: typeof changeSize;
|
||||||
datasourceError: string;
|
datasourceError: string;
|
||||||
datasourceInstance: DataSourceApi;
|
datasourceInstance: DataSourceApi;
|
||||||
|
@ -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',
|
||||||
|
});
|
||||||
|
}
|
@ -583,16 +583,3 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMaxConcurrenShardRequestOrDefault(options: ElasticsearchOptions): number {
|
|
||||||
if (options.maxConcurrentShardRequests === 5 && options.esVersion < 70) {
|
|
||||||
return 256;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.maxConcurrentShardRequests === 256 && options.esVersion >= 70) {
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultMaxConcurrentShardRequests = options.esVersion >= 70 ? 5 : 256;
|
|
||||||
return options.maxConcurrentShardRequests || defaultMaxConcurrentShardRequests;
|
|
||||||
}
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { DataSourcePlugin } from '@grafana/ui';
|
import { DataSourcePlugin } from '@grafana/ui';
|
||||||
import { ElasticDatasource } from './datasource';
|
import { ElasticDatasource } from './datasource';
|
||||||
import { ElasticQueryCtrl } from './query_ctrl';
|
import { ElasticQueryCtrl } from './query_ctrl';
|
||||||
import { ElasticConfigCtrl } from './config_ctrl';
|
|
||||||
import ElasticsearchQueryField from './components/ElasticsearchQueryField';
|
import ElasticsearchQueryField from './components/ElasticsearchQueryField';
|
||||||
|
import { ConfigEditor } from './configuration/ConfigEditor';
|
||||||
|
|
||||||
class ElasticAnnotationsQueryCtrl {
|
class ElasticAnnotationsQueryCtrl {
|
||||||
static templateUrl = 'partials/annotations.editor.html';
|
static templateUrl = 'partials/annotations.editor.html';
|
||||||
@ -10,6 +10,6 @@ class ElasticAnnotationsQueryCtrl {
|
|||||||
|
|
||||||
export const plugin = new DataSourcePlugin(ElasticDatasource)
|
export const plugin = new DataSourcePlugin(ElasticDatasource)
|
||||||
.setQueryCtrl(ElasticQueryCtrl)
|
.setQueryCtrl(ElasticQueryCtrl)
|
||||||
.setConfigCtrl(ElasticConfigCtrl)
|
.setConfigEditor(ConfigEditor)
|
||||||
.setExploreLogsQueryField(ElasticsearchQueryField)
|
.setExploreLogsQueryField(ElasticsearchQueryField)
|
||||||
.setAnnotationQueryCtrl(ElasticAnnotationsQueryCtrl);
|
.setAnnotationQueryCtrl(ElasticAnnotationsQueryCtrl);
|
||||||
|
@ -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>
|
|
@ -1,7 +1,7 @@
|
|||||||
import angular, { IQService } from 'angular';
|
import angular, { IQService } from 'angular';
|
||||||
import { dateMath } from '@grafana/data';
|
import { dateMath } from '@grafana/data';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { ElasticDatasource, getMaxConcurrenShardRequestOrDefault } from '../datasource';
|
import { ElasticDatasource } from '../datasource';
|
||||||
import { toUtc, dateTime } from '@grafana/data';
|
import { toUtc, dateTime } from '@grafana/data';
|
||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
@ -4,13 +4,11 @@ import React, { PureComponent } from 'react';
|
|||||||
// Types
|
// Types
|
||||||
import { InputOptions } from './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 { DataFrame, MutableDataFrame } from '@grafana/data';
|
||||||
import { dataFrameToCSV } from './utils';
|
import { dataFrameToCSV } from './utils';
|
||||||
|
|
||||||
type InputSettings = DataSourceSettings<InputOptions>;
|
interface Props extends DataSourcePluginOptionsEditorProps<InputOptions> {}
|
||||||
|
|
||||||
interface Props extends DataSourcePluginOptionsEditorProps<InputSettings> {}
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -2,14 +2,15 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DataSourceApi, DataSourceJsonData, DataSourceStatus } from '@grafana/ui';
|
import { DataSourceStatus } from '@grafana/ui';
|
||||||
import { LokiQuery } from '../types';
|
import { LokiQuery } from '../types';
|
||||||
import { useLokiSyntax } from './useLokiSyntax';
|
import { useLokiSyntax } from './useLokiSyntax';
|
||||||
import { LokiQueryFieldForm } from './LokiQueryFieldForm';
|
import { LokiQueryFieldForm } from './LokiQueryFieldForm';
|
||||||
|
import LokiDatasource from '../datasource';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
expr: string;
|
expr: string;
|
||||||
datasource: DataSourceApi<LokiQuery, DataSourceJsonData>;
|
datasource: LokiDatasource;
|
||||||
onChange: (expr: string) => void;
|
onChange: (expr: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -15,11 +15,12 @@ import { Plugin, Node } from 'slate';
|
|||||||
// Types
|
// Types
|
||||||
import { LokiQuery } from '../types';
|
import { LokiQuery } from '../types';
|
||||||
import { TypeaheadOutput } from 'app/types/explore';
|
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 { AbsoluteTimeRange } from '@grafana/data';
|
||||||
import { Grammar } from 'prismjs';
|
import { Grammar } from 'prismjs';
|
||||||
import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider';
|
import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider';
|
||||||
import { SuggestionsState } from 'app/features/explore/slate-plugins/suggestions';
|
import { SuggestionsState } from 'app/features/explore/slate-plugins/suggestions';
|
||||||
|
import LokiDatasource from '../datasource';
|
||||||
|
|
||||||
function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) {
|
function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) {
|
||||||
if (datasourceStatus === DataSourceStatus.Disconnected) {
|
if (datasourceStatus === DataSourceStatus.Disconnected) {
|
||||||
@ -68,7 +69,7 @@ export interface CascaderOption {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps<DataSourceApi<LokiQuery>, LokiQuery> {
|
export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps<LokiDatasource, LokiQuery> {
|
||||||
history: LokiHistoryItem[];
|
history: LokiHistoryItem[];
|
||||||
syntax: Grammar;
|
syntax: Grammar;
|
||||||
logLabelOptions: any[];
|
logLabelOptions: any[];
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import LokiDatasource from './datasource';
|
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 {
|
export function makeMockLokiDatasource(labelsAndValues: { [label: string]: string[] }): LokiDatasource {
|
||||||
const labels = Object.keys(labelsAndValues);
|
const labels = Object.keys(labelsAndValues);
|
||||||
@ -25,3 +28,9 @@ export function makeMockLokiDatasource(labelsAndValues: { [label: string]: strin
|
|||||||
},
|
},
|
||||||
} as any;
|
} as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createDefaultConfigOptions(): DataSourceSettings<LokiOptions> {
|
||||||
|
return createDatasourceSettings<LokiOptions>({
|
||||||
|
maxLines: '531',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
|
import { DataSourcePlugin } from '@grafana/ui';
|
||||||
import Datasource from './datasource';
|
import Datasource from './datasource';
|
||||||
|
|
||||||
import LokiCheatSheet from './components/LokiCheatSheet';
|
import LokiCheatSheet from './components/LokiCheatSheet';
|
||||||
import LokiQueryField from './components/LokiQueryField';
|
import LokiQueryField from './components/LokiQueryField';
|
||||||
import LokiQueryEditor from './components/LokiQueryEditor';
|
import LokiQueryEditor from './components/LokiQueryEditor';
|
||||||
import { LokiAnnotationsQueryCtrl } from './LokiAnnotationsQueryCtrl';
|
import { LokiAnnotationsQueryCtrl } from './LokiAnnotationsQueryCtrl';
|
||||||
|
import { ConfigEditor } from './components/ConfigEditor';
|
||||||
|
|
||||||
export class LokiConfigCtrl {
|
export const plugin = new DataSourcePlugin(Datasource)
|
||||||
static templateUrl = 'partials/config.html';
|
.setQueryEditor(LokiQueryEditor)
|
||||||
}
|
.setConfigEditor(ConfigEditor)
|
||||||
|
.setExploreQueryField(LokiQueryField)
|
||||||
export {
|
.setExploreStartPage(LokiCheatSheet)
|
||||||
Datasource,
|
.setAnnotationQueryCtrl(LokiAnnotationsQueryCtrl);
|
||||||
LokiQueryEditor as QueryEditor,
|
|
||||||
LokiConfigCtrl as ConfigCtrl,
|
|
||||||
LokiQueryField as ExploreQueryField,
|
|
||||||
LokiCheatSheet as ExploreStartPage,
|
|
||||||
LokiAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
|
||||||
};
|
|
||||||
|
@ -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>
|
|
@ -1,5 +1,5 @@
|
|||||||
import { Unsubscribable } from 'rxjs';
|
import { Unsubscribable } from 'rxjs';
|
||||||
import { ComponentClass } from 'react';
|
import { ComponentType } from 'react';
|
||||||
import {
|
import {
|
||||||
DataQuery,
|
DataQuery,
|
||||||
DataSourceSelectItem,
|
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.
|
* React component to be shown when no queries have been run yet, e.g., for a query language cheat sheet.
|
||||||
*/
|
*/
|
||||||
StartPage?: ComponentClass<ExploreStartPageProps>;
|
StartPage?: ComponentType<ExploreStartPageProps>;
|
||||||
/**
|
/**
|
||||||
* Width used for calculating the graph interval (can't have more datapoints than pixels)
|
* Width used for calculating the graph interval (can't have more datapoints than pixels)
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user