[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:
parent
c9d19fd43b
commit
6b4becfe02
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
|
|
Loading…
Reference in a new issue