[ML] Data Frame Analytics: Don't allow user to pick an index pattern or saved search based on CCS. (#96555)

Data Frame Analytics does not support cross-cluster search. This PR fixes the SourceSelection component to not allow a user to select a CCS index pattern or a saved search using a CCS index pattern.
This commit is contained in:
Walter Rafelsberger 2021-04-08 17:46:45 +02:00 committed by GitHub
parent c9d19fd43b
commit 6b4becfe02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 291 additions and 3 deletions

View file

@ -0,0 +1,216 @@
/*
* 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 React from 'react';
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import {
getIndexPatternAndSavedSearch,
IndexPatternAndSavedSearch,
} from '../../../../../util/index_utils';
import { SourceSelection } from './source_selection';
jest.mock('../../../../../../../../../../src/plugins/saved_objects/public', () => {
const SavedObjectFinderUi = ({
onChoose,
}: {
onChoose: (id: string, type: string, fullName: string, savedObject: object) => void;
}) => {
return (
<>
<button
onClick={() =>
onChoose('the-remote-index-pattern-id', 'index-pattern', 'the-full-name', {
attributes: { title: 'my_remote_cluster:index-pattern-title' },
})
}
>
RemoteIndexPattern
</button>
<button
onClick={() =>
onChoose('the-plain-index-pattern-id', 'index-pattern', 'the-full-name', {
attributes: { title: 'index-pattern-title' },
})
}
>
PlainIndexPattern
</button>
<button
onClick={() =>
onChoose('the-remote-saved-search-id', 'search', 'the-full-name', {
attributes: { title: 'the-remote-saved-search-title' },
})
}
>
RemoteSavedSearch
</button>
<button
onClick={() =>
onChoose('the-plain-saved-search-id', 'search', 'the-full-name', {
attributes: { title: 'the-plain-saved-search-title' },
})
}
>
PlainSavedSearch
</button>
</>
);
};
return {
SavedObjectFinderUi,
};
});
const mockNavigateToPath = jest.fn();
jest.mock('../../../../../contexts/kibana', () => ({
useMlKibana: () => ({
services: {
savedObjects: {},
uiSettings: {},
},
}),
useNavigateToPath: () => mockNavigateToPath,
}));
jest.mock('../../../../../util/index_utils', () => {
return {
getIndexPatternAndSavedSearch: jest.fn(
async (id: string): Promise<IndexPatternAndSavedSearch> => {
return {
indexPattern: {
fields: [],
title:
id === 'the-remote-saved-search-id'
? 'my_remote_cluster:index-pattern-title'
: 'index-pattern-title',
},
savedSearch: null,
};
}
),
};
});
const mockOnClose = jest.fn();
const mockGetIndexPatternAndSavedSearch = getIndexPatternAndSavedSearch as jest.Mock;
describe('Data Frame Analytics: <SourceSelection />', () => {
afterEach(() => {
mockNavigateToPath.mockClear();
mockGetIndexPatternAndSavedSearch.mockClear();
});
it('renders the title text', async () => {
// prepare
render(
<IntlProvider locale="en">
<SourceSelection onClose={mockOnClose} />
</IntlProvider>
);
// assert
expect(screen.queryByText('New analytics job')).toBeInTheDocument();
expect(mockNavigateToPath).toHaveBeenCalledTimes(0);
expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledTimes(0);
});
it('shows the error callout when clicking a remote index pattern', async () => {
// prepare
render(
<IntlProvider locale="en">
<SourceSelection onClose={mockOnClose} />
</IntlProvider>
);
// act
fireEvent.click(screen.getByText('RemoteIndexPattern', { selector: 'button' }));
await waitFor(() => screen.getByTestId('analyticsCreateSourceIndexModalCcsErrorCallOut'));
// assert
expect(
screen.queryByText('Index patterns using cross-cluster search are not supported.')
).toBeInTheDocument();
expect(mockNavigateToPath).toHaveBeenCalledTimes(0);
expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledTimes(0);
});
it('calls navigateToPath for a plain index pattern ', async () => {
// prepare
render(
<IntlProvider locale="en">
<SourceSelection onClose={mockOnClose} />
</IntlProvider>
);
// act
fireEvent.click(screen.getByText('PlainIndexPattern', { selector: 'button' }));
// assert
await waitFor(() => {
expect(
screen.queryByText('Index patterns using cross-cluster search are not supported.')
).not.toBeInTheDocument();
expect(mockNavigateToPath).toHaveBeenCalledWith(
'/data_frame_analytics/new_job?index=the-plain-index-pattern-id'
);
expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledTimes(0);
});
});
it('shows the error callout when clicking a saved search using a remote index pattern', async () => {
// prepare
render(
<IntlProvider locale="en">
<SourceSelection onClose={mockOnClose} />
</IntlProvider>
);
// act
fireEvent.click(screen.getByText('RemoteSavedSearch', { selector: 'button' }));
await waitFor(() => screen.getByTestId('analyticsCreateSourceIndexModalCcsErrorCallOut'));
// assert
expect(
screen.queryByText('Index patterns using cross-cluster search are not supported.')
).toBeInTheDocument();
expect(
screen.queryByText(
`The saved search 'the-remote-saved-search-title' uses the index pattern 'my_remote_cluster:index-pattern-title'.`
)
).toBeInTheDocument();
expect(mockNavigateToPath).toHaveBeenCalledTimes(0);
expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledWith('the-remote-saved-search-id');
});
it('calls navigateToPath for a saved search using a plain index pattern ', async () => {
// prepare
render(
<IntlProvider locale="en">
<SourceSelection onClose={mockOnClose} />
</IntlProvider>
);
// act
fireEvent.click(screen.getByText('PlainSavedSearch', { selector: 'button' }));
// assert
await waitFor(() => {
expect(
screen.queryByText('Index patterns using cross-cluster search are not supported.')
).not.toBeInTheDocument();
expect(mockNavigateToPath).toHaveBeenCalledWith(
'/data_frame_analytics/new_job?savedSearchId=the-plain-saved-search-id'
);
expect(mockGetIndexPatternAndSavedSearch).toHaveBeenCalledWith('the-plain-saved-search-id');
});
});
});

View file

@ -5,15 +5,28 @@
* 2.0.
*/
import React, { FC } from 'react';
import React, { useState, FC } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui';
import {
EuiCallOut,
EuiModal,
EuiModalBody,
EuiModalHeader,
EuiModalHeaderTitle,
EuiSpacer,
} from '@elastic/eui';
import type { SimpleSavedObject } from 'src/core/public';
import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public';
import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana';
import { getNestedProperty } from '../../../../../util/object_utils';
import { getIndexPatternAndSavedSearch } from '../../../../../util/index_utils';
const fixedPageSize: number = 8;
interface Props {
@ -26,7 +39,49 @@ export const SourceSelection: FC<Props> = ({ onClose }) => {
} = useMlKibana();
const navigateToPath = useNavigateToPath();
const onSearchSelected = async (id: string, type: string) => {
const [isCcsCallOut, setIsCcsCallOut] = useState(false);
const [ccsCallOutBodyText, setCcsCallOutBodyText] = useState<string>();
const onSearchSelected = async (
id: string,
type: string,
fullName: string,
savedObject: SimpleSavedObject
) => {
// Kibana index patterns including `:` are cross-cluster search indices
// and are not supported by Data Frame Analytics yet. For saved searches
// and index patterns that use cross-cluster search we intercept
// the selection before redirecting and show an error callout instead.
let indexPatternTitle = '';
if (type === 'index-pattern') {
indexPatternTitle = getNestedProperty(savedObject, 'attributes.title');
} else if (type === 'search') {
const indexPatternAndSavedSearch = await getIndexPatternAndSavedSearch(id);
indexPatternTitle = indexPatternAndSavedSearch.indexPattern?.title ?? '';
}
if (indexPatternTitle.includes(':')) {
setIsCcsCallOut(true);
if (type === 'search') {
setCcsCallOutBodyText(
i18n.translate(
'xpack.ml.dataFrame.analytics.create.searchSelection.CcsErrorCallOutBody',
{
defaultMessage: `The saved search '{savedSearchTitle}' uses the index pattern '{indexPatternTitle}'.`,
values: {
savedSearchTitle: getNestedProperty(savedObject, 'attributes.title'),
indexPatternTitle,
},
}
)
);
} else {
setCcsCallOutBodyText(undefined);
}
return;
}
await navigateToPath(
`/data_frame_analytics/new_job?${
type === 'index-pattern' ? 'index' : 'savedSearchId'
@ -54,6 +109,23 @@ export const SourceSelection: FC<Props> = ({ onClose }) => {
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
{isCcsCallOut && (
<>
<EuiCallOut
data-test-subj="analyticsCreateSourceIndexModalCcsErrorCallOut"
title={i18n.translate(
'xpack.ml.dataFrame.analytics.create.searchSelection.CcsErrorCallOutTitle',
{
defaultMessage: 'Index patterns using cross-cluster search are not supported.',
}
)}
color="danger"
>
{typeof ccsCallOutBodyText === 'string' && <p>{ccsCallOutBodyText}</p>}
</EuiCallOut>
<EuiSpacer size="m" />
</>
)}
<SavedObjectFinderUi
key="searchSavedObjectFinder"
onChoose={onSearchSelected}