diff --git a/packages/grafana-ui/src/components/Forms/InlineField.tsx b/packages/grafana-ui/src/components/Forms/InlineField.tsx index 1f3291f658c..60ff61e36a5 100644 --- a/packages/grafana-ui/src/components/Forms/InlineField.tsx +++ b/packages/grafana-ui/src/components/Forms/InlineField.tsx @@ -1,11 +1,12 @@ import React, { FC } from 'react'; import { cx, css } from '@emotion/css'; -import { GrafanaTheme } from '@grafana/data'; -import { useTheme } from '../../themes'; +import { GrafanaTheme2 } from '@grafana/data'; +import { useTheme2 } from '../../themes'; import { InlineLabel } from './InlineLabel'; import { PopoverContent } from '../Tooltip/Tooltip'; import { FieldProps } from './Field'; import { getChildId } from '../../utils/reactUtils'; +import { FieldValidationMessage } from './FieldValidationMessage'; export interface Props extends Omit { /** Content for the label's tooltip */ @@ -16,6 +17,8 @@ export interface Props extends Omit = ({ className, htmlFor, grow, + error, transparent, ...htmlProps }) => { - const theme = useTheme(); + const theme = useTheme2(); const styles = getStyles(theme, grow); const inputId = htmlFor ?? getChildId(children); @@ -49,14 +53,21 @@ export const InlineField: FC = ({ return (
{labelElement} - {React.cloneElement(children, { invalid, disabled, loading })} +
+ {React.cloneElement(children, { invalid, disabled, loading })} + {invalid && error && ( +
+ {error} +
+ )} +
); }; InlineField.displayName = 'InlineField'; -const getStyles = (theme: GrafanaTheme, grow?: boolean) => { +const getStyles = (theme: GrafanaTheme2, grow?: boolean) => { return { container: css` display: flex; @@ -65,7 +76,13 @@ const getStyles = (theme: GrafanaTheme, grow?: boolean) => { text-align: left; position: relative; flex: ${grow ? 1 : 0} 0 auto; - margin: 0 ${theme.spacing.xs} ${theme.spacing.xs} 0; + margin: 0 ${theme.spacing(0.5)} ${theme.spacing(0.5)} 0; + `, + childContainer: css` + flex: ${grow ? 1 : 0} 0 auto; + `, + fieldValidationWrapper: css` + margin-top: ${theme.spacing(0.5)}; `, }; }; diff --git a/public/app/core/components/TransformersUI/configFromQuery/ConfigFromQueryTransformerEditor.test.tsx b/public/app/core/components/TransformersUI/configFromQuery/ConfigFromQueryTransformerEditor.test.tsx index 6d5a221b89d..863d0f0c0cc 100644 --- a/public/app/core/components/TransformersUI/configFromQuery/ConfigFromQueryTransformerEditor.test.tsx +++ b/public/app/core/components/TransformersUI/configFromQuery/ConfigFromQueryTransformerEditor.test.tsx @@ -38,7 +38,7 @@ describe('ConfigFromQueryTransformerEditor', () => { it('Should be able to select config frame by refId', async () => { setup(); - let select = (await screen.findByText('Config query')).nextSibling!; + let select = (await screen.findByText('Config query')).nextSibling!.firstChild!; await fireEvent.keyDown(select, { keyCode: 40 }); await selectOptionInTest(select as HTMLElement, 'A'); diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx index 3b5da1fe4b4..95996d3b892 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx @@ -1,92 +1,89 @@ import React from 'react'; -import { last } from 'lodash'; -import { mount } from 'enzyme'; import { ElasticDetails } from './ElasticDetails'; import { createDefaultConfigOptions } from './mocks'; -import { LegacyForms } from '@grafana/ui'; -const { Select } = LegacyForms; +import { render, screen } from '@testing-library/react'; +import selectEvent from 'react-select-event'; 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); - }); + describe('Max concurrent Shard Requests', () => { + it('should render "Max concurrent Shard Requests" if version >= 5.6.0', () => { + render( {}} value={createDefaultConfigOptions({ esVersion: '5.6.0' })} />); + expect(screen.getByLabelText('Max concurrent Shard Requests')).toBeInTheDocument(); + }); - it('should not render "Max concurrent Shard Requests" if version is low', () => { - const options = createDefaultConfigOptions(); - options.jsonData.esVersion = '5.0.0'; - const wrapper = mount( {}} value={options} />); - expect(wrapper.find('input[aria-label="Max concurrent Shard Requests input"]').length).toBe(0); + it('should not render "Max concurrent Shard Requests" if version < 5.6.0', () => { + render( {}} value={createDefaultConfigOptions({ esVersion: '5.0.0' })} />); + expect(screen.queryByLabelText('Max concurrent Shard Requests')).not.toBeInTheDocument(); + }); }); - it('should change database on interval change when not set explicitly', () => { + it('should change database on interval change when not set explicitly', async () => { const onChangeMock = jest.fn(); - const wrapper = mount(); - const selectEl = wrapper.find({ label: 'Pattern' }).find(Select); - selectEl.props().onChange({ value: 'Daily', label: 'Daily' }, { action: 'select-option', option: undefined }); + render(); + const selectEl = screen.getByLabelText('Pattern'); - expect(onChangeMock.mock.calls[0][0].jsonData.interval).toBe('Daily'); - expect(onChangeMock.mock.calls[0][0].database).toBe('[logstash-]YYYY.MM.DD'); + await selectEvent.select(selectEl, 'Daily', { container: document.body }); + + expect(onChangeMock).toHaveBeenLastCalledWith( + expect.objectContaining({ + database: '[logstash-]YYYY.MM.DD', + jsonData: expect.objectContaining({ interval: 'Daily' }), + }) + ); }); - it('should change database on interval change if pattern is from example', () => { + it('should change database on interval change if pattern is from example', async () => { const onChangeMock = jest.fn(); const options = createDefaultConfigOptions(); options.database = '[logstash-]YYYY.MM.DD.HH'; - const wrapper = mount(); + render(); + const selectEl = screen.getByLabelText('Pattern'); - const selectEl = wrapper.find({ label: 'Pattern' }).find(Select); - selectEl.props().onChange({ value: 'Monthly', label: 'Monthly' }, { action: 'select-option', option: undefined }); + await selectEvent.select(selectEl, 'Monthly', { container: document.body }); - expect(onChangeMock.mock.calls[0][0].jsonData.interval).toBe('Monthly'); - expect(onChangeMock.mock.calls[0][0].database).toBe('[logstash-]YYYY.MM'); + expect(onChangeMock).toHaveBeenLastCalledWith( + expect.objectContaining({ + database: '[logstash-]YYYY.MM', + jsonData: expect.objectContaining({ interval: 'Monthly' }), + }) + ); }); describe('version change', () => { const testCases = [ - { version: '5.0.0', expectedMaxConcurrentShardRequests: 256 }, - { version: '5.0.0', maxConcurrentShardRequests: 50, expectedMaxConcurrentShardRequests: 50 }, - { version: '5.6.0', expectedMaxConcurrentShardRequests: 256 }, - { version: '5.6.0', maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 256 }, - { version: '5.6.0', maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 256 }, - { version: '5.6.0', maxConcurrentShardRequests: 200, expectedMaxConcurrentShardRequests: 200 }, - { version: '7.0.0', expectedMaxConcurrentShardRequests: 5 }, - { version: '7.0.0', maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 5 }, - { version: '7.0.0', maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 5 }, - { version: '7.0.0', maxConcurrentShardRequests: 6, expectedMaxConcurrentShardRequests: 6 }, + { version: '5.x', expectedMaxConcurrentShardRequests: 256 }, + { version: '5.x', maxConcurrentShardRequests: 50, expectedMaxConcurrentShardRequests: 50 }, + { version: '5.6+', expectedMaxConcurrentShardRequests: 256 }, + { version: '5.6+', maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 256 }, + { version: '5.6+', maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 256 }, + { version: '5.6+', maxConcurrentShardRequests: 200, expectedMaxConcurrentShardRequests: 200 }, + { version: '7.0+', expectedMaxConcurrentShardRequests: 5 }, + { version: '7.0+', maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 5 }, + { version: '7.0+', maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 5 }, + { version: '7.0+', 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, + const onChangeMock = jest.fn(); + it(`sets maxConcurrentShardRequests=${tc.expectedMaxConcurrentShardRequests} if version=${tc.version},`, async () => { + render( + + ); + + const selectEl = screen.getByLabelText('ElasticSearch version'); - const selectEl = wrapper.find({ label: 'Version' }).find(Select); - selectEl - .props() - .onChange( - { value: tc.version, label: tc.version.toString() }, - { action: 'select-option', option: undefined } - ); + await selectEvent.select(selectEl, tc.version, { container: document.body }); - expect(last(onChangeMock.mock.calls)[0].jsonData.maxConcurrentShardRequests).toBe( - tc.expectedMaxConcurrentShardRequests + expect(onChangeMock).toHaveBeenCalledWith( + expect.objectContaining({ + jsonData: expect.objectContaining({ maxConcurrentShardRequests: tc.expectedMaxConcurrentShardRequests }), + }) ); }); }); diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx index 6dbeb288ac0..f0016bd9f12 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { EventsWithValidation, regexValidation, LegacyForms } from '@grafana/ui'; -const { Switch, Select, Input, FormField } = LegacyForms; +import { FieldSet, InlineField, Input, Select, InlineSwitch } from '@grafana/ui'; import { ElasticsearchOptions, Interval } from '../types'; import { DataSourceSettings, SelectableValue } from '@grafana/data'; import { gte, lt, valid } from 'semver'; @@ -45,140 +44,116 @@ export const ElasticDetails = ({ value, onChange }: Props) => { : undefined; 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={currentVersion || customOption} - /> - } + + + + -
+ )} -
-
- - } - 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. - - } - /> -
-
-
- + 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. + + } + error="Value is not valid, you can use number with time unit specifier: y, M, w, d, h, m, s" + invalid={!!value.jsonData.timeInterval && !/^\d+(ms|[Mwdhmsy])$/.test(value.jsonData.timeInterval)} + > + + + + + -
+ {gte(value.jsonData.esVersion, '6.6.0') && value.jsonData.xpack && ( -
- + -
+ )} - + ); }; diff --git a/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.test.tsx b/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.test.tsx index eab865a458c..4e967640d1d 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.test.tsx @@ -1,26 +1,22 @@ import React from 'react'; -import { mount, shallow } from 'enzyme'; +import { render, screen, fireEvent } from '@testing-library/react'; import { LogsConfig } from './LogsConfig'; import { createDefaultConfigOptions } from './mocks'; -import { LegacyForms } from '@grafana/ui'; -const { FormField } = LegacyForms; 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'); + const expectedMessageField = '@message'; + const expectedLevelField = '@level'; + + render(); + const messageField = screen.getByLabelText('Message field name'); + const levelField = screen.getByLabelText('Level field name'); + + fireEvent.change(messageField, { target: { value: expectedMessageField } }); + expect(onChangeMock).toHaveBeenLastCalledWith(expect.objectContaining({ logMessageField: expectedMessageField })); + + fireEvent.change(levelField, { target: { value: expectedLevelField } }); + expect(onChangeMock).toHaveBeenLastCalledWith(expect.objectContaining({ logLevelField: expectedLevelField })); }); }); diff --git a/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.tsx b/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.tsx index 67d547e7da8..155dc360e97 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { LegacyForms } from '@grafana/ui'; -const { FormField } = LegacyForms; +import { Input, InlineField, FieldSet } from '@grafana/ui'; import { ElasticsearchOptions } from '../types'; type Props = { @@ -19,28 +18,25 @@ export const LogsConfig = (props: Props) => { }; return ( - <> -

Logs

+
+ + + -
-
- -
-
- -
-
- + + + +
); }; diff --git a/public/app/plugins/datasource/elasticsearch/configuration/mocks.ts b/public/app/plugins/datasource/elasticsearch/configuration/mocks.ts index f5c5059319a..f80d702d3e3 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/mocks.ts +++ b/public/app/plugins/datasource/elasticsearch/configuration/mocks.ts @@ -2,7 +2,9 @@ import { DataSourceSettings } from '@grafana/data'; import { ElasticsearchOptions } from '../types'; import { createDatasourceSettings } from '../../../../features/datasources/mocks'; -export function createDefaultConfigOptions(): DataSourceSettings { +export function createDefaultConfigOptions( + options?: Partial +): DataSourceSettings { return createDatasourceSettings({ timeField: '@time', esVersion: '7.0.0', @@ -11,5 +13,6 @@ export function createDefaultConfigOptions(): DataSourceSettings { it('should switch scenario and display its default values', async () => { const { rerender } = setup(); - let select = (await screen.findByText('Scenario')).nextSibling!; + let select = (await screen.findByText('Scenario')).nextSibling!.firstChild!; await fireEvent.keyDown(select, { keyCode: 40 }); const scs = screen.getAllByLabelText('Select option');