mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Discover] Add support for copying the query from the add rule flyout (#135098)
This commit is contained in:
parent
2a036506b2
commit
850b8f015b
4 changed files with 317 additions and 22 deletions
|
@ -18,8 +18,18 @@ import { Subject } from 'rxjs';
|
|||
import { ISearchSource } from '@kbn/data-plugin/common';
|
||||
import { IUiSettingsClient } from '@kbn/core/public';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { copyToClipboard, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
return {
|
||||
__esModule: true,
|
||||
...original,
|
||||
copyToClipboard: jest.fn(() => true),
|
||||
};
|
||||
});
|
||||
|
||||
const dataViewPluginMock = dataViewPluginMocks.createStartContract();
|
||||
const chartsStartMock = chartPluginMock.createStartContract();
|
||||
|
@ -83,6 +93,69 @@ const searchSourceMock = {
|
|||
fetch$: jest.fn(() => {
|
||||
return mockSearchResult;
|
||||
}),
|
||||
getSearchRequestBody: jest.fn(() => ({
|
||||
fields: [
|
||||
{
|
||||
field: '@timestamp',
|
||||
format: 'date_time',
|
||||
},
|
||||
{
|
||||
field: 'timestamp',
|
||||
format: 'date_time',
|
||||
},
|
||||
{
|
||||
field: 'utc_time',
|
||||
format: 'date_time',
|
||||
},
|
||||
],
|
||||
script_fields: {},
|
||||
stored_fields: ['*'],
|
||||
runtime_mappings: {
|
||||
hour_of_day: {
|
||||
type: 'long',
|
||||
script: {
|
||||
source: "emit(doc['timestamp'].value.getHour());",
|
||||
},
|
||||
},
|
||||
},
|
||||
_source: {
|
||||
excludes: [],
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
must: [],
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
response: '200',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
timestamp: {
|
||||
format: 'strict_date_optional_time',
|
||||
gte: '2022-06-19T02:49:51.192Z',
|
||||
lte: '2022-06-24T02:49:51.192Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
should: [],
|
||||
must_not: [],
|
||||
},
|
||||
},
|
||||
})),
|
||||
} as unknown as ISearchSource;
|
||||
|
||||
const savedQueryMock = {
|
||||
|
@ -179,6 +252,83 @@ describe('SearchSourceAlertTypeExpression', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should call copyToClipboard with the serialized query when the copy query button is clicked', async () => {
|
||||
let wrapper = null as unknown as ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = setup(defaultSearchSourceExpressionParams);
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
findTestSubject(wrapper, 'copyQuery').simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
expect(copyToClipboard).toHaveBeenCalledWith(`{
|
||||
\"fields\": [
|
||||
{
|
||||
\"field\": \"@timestamp\",
|
||||
\"format\": \"date_time\"
|
||||
},
|
||||
{
|
||||
\"field\": \"timestamp\",
|
||||
\"format\": \"date_time\"
|
||||
},
|
||||
{
|
||||
\"field\": \"utc_time\",
|
||||
\"format\": \"date_time\"
|
||||
}
|
||||
],
|
||||
\"script_fields\": {},
|
||||
\"stored_fields\": [
|
||||
\"*\"
|
||||
],
|
||||
\"runtime_mappings\": {
|
||||
\"hour_of_day\": {
|
||||
\"type\": \"long\",
|
||||
\"script\": {
|
||||
\"source\": \"emit(doc['timestamp'].value.getHour());\"
|
||||
}
|
||||
}
|
||||
},
|
||||
\"_source\": {
|
||||
\"excludes\": []
|
||||
},
|
||||
\"query\": {
|
||||
\"bool\": {
|
||||
\"must\": [],
|
||||
\"filter\": [
|
||||
{
|
||||
\"bool\": {
|
||||
\"must_not\": {
|
||||
\"bool\": {
|
||||
\"should\": [
|
||||
{
|
||||
\"match\": {
|
||||
\"response\": \"200\"
|
||||
}
|
||||
}
|
||||
],
|
||||
\"minimum_should_match\": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\"range\": {
|
||||
\"timestamp\": {
|
||||
\"format\": \"strict_date_optional_time\",
|
||||
\"gte\": \"2022-06-19T02:49:51.192Z\",
|
||||
\"lte\": \"2022-06-24T02:49:51.192Z\"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
\"should\": [],
|
||||
\"must_not\": []
|
||||
}
|
||||
}
|
||||
}`);
|
||||
});
|
||||
|
||||
test('should render error prompt', async () => {
|
||||
(dataMock.search.searchSource.create as jest.Mock).mockImplementationOnce(() =>
|
||||
Promise.reject(new Error('Cant find searchSource'))
|
||||
|
|
|
@ -172,8 +172,10 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
|
|||
(updatedValue: number) => dispatch({ type: 'size', payload: updatedValue }),
|
||||
[]
|
||||
);
|
||||
const onTestFetch = useCallback(async () => {
|
||||
const timeWindow = `${timeWindowSize}${timeWindowUnit}`;
|
||||
|
||||
const timeWindow = `${timeWindowSize}${timeWindowUnit}`;
|
||||
|
||||
const createTestSearchSource = useCallback(() => {
|
||||
const testSearchSource = searchSource.createCopy();
|
||||
const timeFilter = getTime(searchSource.getField('index')!, {
|
||||
from: `now-${timeWindow}`,
|
||||
|
@ -183,9 +185,19 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
|
|||
'filter',
|
||||
timeFilter ? [timeFilter, ...ruleConfiguration.filter] : ruleConfiguration.filter
|
||||
);
|
||||
return testSearchSource;
|
||||
}, [searchSource, timeWindow, ruleConfiguration]);
|
||||
|
||||
const onCopyQuery = useCallback(() => {
|
||||
const testSearchSource = createTestSearchSource();
|
||||
return JSON.stringify(testSearchSource.getSearchRequestBody(), null, 2);
|
||||
}, [createTestSearchSource]);
|
||||
|
||||
const onTestFetch = useCallback(async () => {
|
||||
const testSearchSource = createTestSearchSource();
|
||||
const { rawResponse } = await lastValueFrom(testSearchSource.fetch$());
|
||||
return { nrOfDocs: totalHitsToNumber(rawResponse.hits.total), timeWindow };
|
||||
}, [searchSource, timeWindowSize, timeWindowUnit, ruleConfiguration]);
|
||||
}, [timeWindow, createTestSearchSource]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
@ -289,7 +301,7 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
|
|||
onChangeSelectedValue={onChangeSizeValue}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<TestQueryRow fetch={onTestFetch} hasValidationErrors={false} />
|
||||
<TestQueryRow fetch={onTestFetch} copyQuery={onCopyQuery} hasValidationErrors={false} />
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { copyToClipboard } from '@elastic/eui';
|
||||
import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { TestQueryRow } from './test_query_row';
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
return {
|
||||
__esModule: true,
|
||||
...original,
|
||||
copyToClipboard: jest.fn(() => true),
|
||||
};
|
||||
});
|
||||
|
||||
const COPIED_QUERY = 'COPIED QUERY';
|
||||
const onFetch = () => Promise.resolve({ nrOfDocs: 42, timeWindow: '5m' });
|
||||
const onCopyQuery = () => COPIED_QUERY;
|
||||
|
||||
describe('TestQueryRow', () => {
|
||||
it('should render the copy query button if copyQuery is provided', () => {
|
||||
const component = mountWithIntl(
|
||||
<TestQueryRow fetch={onFetch} copyQuery={onCopyQuery} hasValidationErrors={false} />
|
||||
);
|
||||
expect(findTestSubject(component, 'copyQuery').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not render the copy query button if copyQuery is not provided', () => {
|
||||
const component = mountWithIntl(<TestQueryRow fetch={onFetch} hasValidationErrors={false} />);
|
||||
expect(findTestSubject(component, 'copyQuery').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should disable the test query and copy query buttons if hasValidationErrors is true', () => {
|
||||
const component = mountWithIntl(
|
||||
<TestQueryRow fetch={onFetch} copyQuery={onCopyQuery} hasValidationErrors={true} />
|
||||
);
|
||||
expect(findTestSubject(component, 'testQuery').prop('disabled')).toBe(true);
|
||||
expect(findTestSubject(component, 'copyQuery').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not disable the test query and copy query buttons if hasValidationErrors is false', () => {
|
||||
const component = mountWithIntl(
|
||||
<TestQueryRow fetch={onFetch} copyQuery={onCopyQuery} hasValidationErrors={false} />
|
||||
);
|
||||
expect(findTestSubject(component, 'testQuery').prop('disabled')).toBe(false);
|
||||
expect(findTestSubject(component, 'copyQuery').prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('should call the fetch callback when the test query button is clicked', async () => {
|
||||
const localOnFetch = jest.fn(onFetch);
|
||||
const component = mountWithIntl(
|
||||
<TestQueryRow fetch={localOnFetch} hasValidationErrors={false} />
|
||||
);
|
||||
await act(async () => {
|
||||
findTestSubject(component, 'testQuery').simulate('click');
|
||||
});
|
||||
expect(localOnFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call the copyQuery callback and pass the returned value to copyToClipboard when the copy query button is clicked', async () => {
|
||||
const localOnCopyQuery = jest.fn(onCopyQuery);
|
||||
const component = mountWithIntl(
|
||||
<TestQueryRow fetch={onFetch} copyQuery={localOnCopyQuery} hasValidationErrors={false} />
|
||||
);
|
||||
await act(async () => {
|
||||
findTestSubject(component, 'copyQuery').simulate('click');
|
||||
});
|
||||
component.update();
|
||||
expect(localOnCopyQuery).toHaveBeenCalled();
|
||||
expect(copyToClipboard).toHaveBeenCalledWith(COPIED_QUERY);
|
||||
});
|
||||
});
|
|
@ -4,38 +4,92 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiButton, EuiFormRow, EuiText } from '@elastic/eui';
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import {
|
||||
copyToClipboard,
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useTestQuery } from './use_test_query';
|
||||
|
||||
export function TestQueryRow({
|
||||
fetch,
|
||||
copyQuery,
|
||||
hasValidationErrors,
|
||||
}: {
|
||||
fetch: () => Promise<{ nrOfDocs: number; timeWindow: string }>;
|
||||
copyQuery?: () => string;
|
||||
hasValidationErrors: boolean;
|
||||
}) {
|
||||
const { onTestQuery, testQueryResult, testQueryError, testQueryLoading } = useTestQuery(fetch);
|
||||
const [copiedMessage, setCopiedMessage] = useState<ReactNode | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow>
|
||||
<EuiButton
|
||||
data-test-subj="testQuery"
|
||||
color="primary"
|
||||
iconSide="left"
|
||||
iconType="playFilled"
|
||||
onClick={onTestQuery}
|
||||
disabled={hasValidationErrors}
|
||||
isLoading={testQueryLoading}
|
||||
size="s"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.esQuery.ui.testQuery"
|
||||
defaultMessage="Test query"
|
||||
/>
|
||||
</EuiButton>
|
||||
<EuiFlexGroup alignItems="center" responsive={false} gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="testQuery"
|
||||
color="primary"
|
||||
iconSide="left"
|
||||
iconType="playFilled"
|
||||
onClick={() => {
|
||||
onTestQuery();
|
||||
}}
|
||||
disabled={hasValidationErrors}
|
||||
isLoading={testQueryLoading}
|
||||
size="s"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.esQuery.ui.testQuery"
|
||||
defaultMessage="Test query"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
{copyQuery && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={copiedMessage}
|
||||
onMouseOut={() => {
|
||||
setCopiedMessage(null);
|
||||
}}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="copyQuery"
|
||||
color="primary"
|
||||
iconSide="left"
|
||||
iconType="copyClipboard"
|
||||
onClick={() => {
|
||||
const copied = copyToClipboard(copyQuery());
|
||||
if (copied) {
|
||||
setCopiedMessage(
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.esQuery.ui.queryCopiedToClipboard"
|
||||
defaultMessage="Copied"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
disabled={hasValidationErrors}
|
||||
isLoading={testQueryLoading}
|
||||
size="s"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.esQuery.ui.copyQuery"
|
||||
defaultMessage="Copy query"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
{testQueryLoading && (
|
||||
<EuiFormRow>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue