diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx
new file mode 100644
index 0000000000000..858ab58b53f4b
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.test.tsx
@@ -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 (
+ <>
+
+
+
+
+ >
+ );
+ };
+
+ 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 => {
+ 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: ', () => {
+ afterEach(() => {
+ mockNavigateToPath.mockClear();
+ mockGetIndexPatternAndSavedSearch.mockClear();
+ });
+
+ it('renders the title text', async () => {
+ // prepare
+ render(
+
+
+
+ );
+
+ // 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(
+
+
+
+ );
+
+ // 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(
+
+
+
+ );
+
+ // 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(
+
+
+
+ );
+
+ // 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(
+
+
+
+ );
+
+ // 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');
+ });
+ });
+});
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx
index 40f97690d7790..cbc5a226eb319 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx
@@ -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 = ({ onClose }) => {
} = useMlKibana();
const navigateToPath = useNavigateToPath();
- const onSearchSelected = async (id: string, type: string) => {
+ const [isCcsCallOut, setIsCcsCallOut] = useState(false);
+ const [ccsCallOutBodyText, setCcsCallOutBodyText] = useState();
+
+ 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 = ({ onClose }) => {
+ {isCcsCallOut && (
+ <>
+
+ {typeof ccsCallOutBodyText === 'string' && {ccsCallOutBodyText}
}
+
+
+ >
+ )}