Skip to content

Commit

Permalink
[ML] Data Frame Analytics: Don't allow user to pick an index pattern …
Browse files Browse the repository at this point in the history
…or saved search based on CCS. (elastic#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.
  • Loading branch information
walterra authored and kibanamachine committed Apr 8, 2021
1 parent 7fcddec commit a088e20
Show file tree
Hide file tree
Showing 2 changed files with 291 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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'
Expand Down Expand Up @@ -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}
Expand Down

0 comments on commit a088e20

Please sign in to comment.