[ML] Transforms: Wizard displays warning callout for source preview when used with CCS against clusters below 7.10. (#96297)

The transforms UI source preview uses fields to search and retrieve document attributes. The feature was introduced in 7.10. For cross cluster search, as of now, when a search using fields is used using cross cluster search against a cluster earlier than 7.10, the API won't return an error or other information but just silently drop the fields attribute and return empty hits without field attributes.

In Kibana, index patterns can be set up to use cross cluster search using the pattern <cluster-names>:<pattern>. If we identify such a pattern and the search hits don't include fields attributes, we display a warning callout from now on.
This commit is contained in:
Walter Rafelsberger 2021-04-07 16:39:11 +02:00 committed by GitHub
parent 7f4ec48ce6
commit f945f3a425
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 10 deletions

View file

@ -80,6 +80,7 @@ export const DataGrid: FC<Props> = memo(
baseline,
chartsVisible,
chartsButtonVisible,
ccsWarning,
columnsWithCharts,
dataTestSubj,
errorMessage,
@ -291,6 +292,24 @@ export const DataGrid: FC<Props> = memo(
<EuiSpacer size="m" />
</div>
)}
{ccsWarning && (
<div data-test-subj={`${dataTestSubj} ccsWarning`}>
<EuiCallOut
title={i18n.translate('xpack.ml.dataGrid.CcsWarningCalloutTitle', {
defaultMessage: 'Cross-cluster search returned no fields data.',
})}
color="warning"
>
<p>
{i18n.translate('xpack.ml.dataGrid.CcsWarningCalloutBody', {
defaultMessage:
'There was an issue retrieving data for the index pattern. Source preview in combination with cross-cluster search is only supported for versions 7.10 and above. You may still configure and create the transform.',
})}
</p>
</EuiCallOut>
<EuiSpacer size="m" />
</div>
)}
<div className="mlDataGrid">
<EuiDataGrid
aria-label={isWithHeader(props) ? props.title : ''}

View file

@ -59,6 +59,7 @@ export interface UseIndexDataReturnType
UseDataGridReturnType,
| 'chartsVisible'
| 'chartsButtonVisible'
| 'ccsWarning'
| 'columnsWithCharts'
| 'errorMessage'
| 'invalidSortingColumnns'
@ -84,6 +85,7 @@ export interface UseIndexDataReturnType
}
export interface UseDataGridReturnType {
ccsWarning: boolean;
chartsVisible: ChartsVisible;
chartsButtonVisible: boolean;
columnsWithCharts: EuiDataGridColumn[];
@ -97,6 +99,7 @@ export interface UseDataGridReturnType {
resetPagination: () => void;
rowCount: number;
rowCountRelation: RowCountRelation;
setCcsWarning: Dispatch<SetStateAction<boolean>>;
setColumnCharts: Dispatch<SetStateAction<ChartData[]>>;
setErrorMessage: Dispatch<SetStateAction<string>>;
setNoDataMessage: Dispatch<SetStateAction<string>>;

View file

@ -36,6 +36,7 @@ export const useDataGrid = (
): UseDataGridReturnType => {
const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: defaultPageSize };
const [ccsWarning, setCcsWarning] = useState(false);
const [noDataMessage, setNoDataMessage] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [status, setStatus] = useState(INDEX_STATUS.UNUSED);
@ -152,6 +153,7 @@ export const useDataGrid = (
}, [chartsVisible, rowCount, rowCountRelation]);
return {
ccsWarning,
chartsVisible,
chartsButtonVisible: true,
columnsWithCharts,
@ -166,6 +168,7 @@ export const useDataGrid = (
rowCount,
rowCountRelation,
setColumnCharts,
setCcsWarning,
setErrorMessage,
setNoDataMessage,
setPagination,

View file

@ -136,9 +136,20 @@ const apiFactory = () => ({
return Promise.resolve([]);
},
async esSearch(payload: any): Promise<estypes.SearchResponse | HttpFetchError> {
const hits = [];
// simulate a cross cluster search result
// against a cluster that doesn't support fields
if (payload.index.includes(':')) {
hits.push({
_id: 'the-doc',
_index: 'the-index',
});
}
return Promise.resolve({
hits: {
hits: [],
hits,
total: {
value: 0,
relation: 'eq',

View file

@ -7,7 +7,8 @@
import React, { FC } from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { render, waitFor } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { CoreSetup } from 'src/core/public';
@ -49,6 +50,7 @@ describe('Transform: useIndexData()', () => {
const wrapper: FC = ({ children }) => (
<MlSharedContext.Provider value={mlShared}>{children}</MlSharedContext.Provider>
);
const { result, waitForNextUpdate } = renderHook(
() =>
useIndexData(
@ -62,6 +64,7 @@ describe('Transform: useIndexData()', () => {
),
{ wrapper }
);
const IndexObj: UseIndexDataReturnType = result.current;
await waitForNextUpdate();
@ -73,7 +76,7 @@ describe('Transform: useIndexData()', () => {
});
describe('Transform: <DataGrid /> with useIndexData()', () => {
test('Minimal initialization', async () => {
test('Minimal initialization, no cross cluster search warning.', async () => {
// Arrange
const indexPattern = {
title: 'the-index-pattern-title',
@ -97,7 +100,8 @@ describe('Transform: <DataGrid /> with useIndexData()', () => {
return <DataGrid {...props} />;
};
const { getByText } = render(
const { queryByText } = render(
<MlSharedContext.Provider value={mlSharedImports}>
<Wrapper />
</MlSharedContext.Provider>
@ -105,6 +109,48 @@ describe('Transform: <DataGrid /> with useIndexData()', () => {
// Act
// Assert
expect(getByText('the-index-preview-title')).toBeInTheDocument();
await waitFor(() => {
expect(queryByText('the-index-preview-title')).toBeInTheDocument();
expect(queryByText('Cross-cluster search returned no fields data.')).not.toBeInTheDocument();
});
});
test('Cross-cluster search warning', async () => {
// Arrange
const indexPattern = {
title: 'remote:the-index-pattern-title',
fields: [] as any[],
} as SearchItems['indexPattern'];
const mlSharedImports = await getMlSharedImports();
const Wrapper = () => {
const {
ml: { DataGrid },
} = useAppDependencies();
const props = {
...useIndexData(indexPattern, { match_all: {} }, runtimeMappings),
copyToClipboard: 'the-copy-to-clipboard-code',
copyToClipboardDescription: 'the-copy-to-clipboard-description',
dataTestSubj: 'the-data-test-subj',
title: 'the-index-preview-title',
toastNotifications: {} as CoreSetup['notifications']['toasts'],
};
return <DataGrid {...props} />;
};
const { queryByText } = render(
<MlSharedContext.Provider value={mlSharedImports}>
<Wrapper />
</MlSharedContext.Provider>
);
// Act
// Assert
await waitFor(() => {
expect(queryByText('the-index-preview-title')).toBeInTheDocument();
expect(queryByText('Cross-cluster search returned no fields data.')).toBeInTheDocument();
});
});
});

View file

@ -87,6 +87,7 @@ export const useIndexData = (
pagination,
resetPagination,
setColumnCharts,
setCcsWarning,
setErrorMessage,
setRowCount,
setRowCountRelation,
@ -134,8 +135,12 @@ export const useIndexData = (
return;
}
const isCrossClusterSearch = indexPattern.title.includes(':');
const isMissingFields = resp.hits.hits.every((d) => typeof d.fields === 'undefined');
const docs = resp.hits.hits.map((d) => getProcessedFields(d.fields ?? {}));
setCcsWarning(isCrossClusterSearch && isMissingFields);
setRowCount(typeof resp.hits.total === 'number' ? resp.hits.total : resp.hits.total.value);
setRowCountRelation(
typeof resp.hits.total === 'number'

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { render, wait } from '@testing-library/react';
import { render, waitFor } from '@testing-library/react';
import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs';
@ -77,7 +77,7 @@ describe('Transform: <DefinePivotSummary />', () => {
},
};
const { getByText } = render(
const { queryByText } = render(
<MlSharedContext.Provider value={mlSharedImports}>
<StepDefineSummary formState={formState} searchItems={searchItems as SearchItems} />
</MlSharedContext.Provider>
@ -85,8 +85,9 @@ describe('Transform: <DefinePivotSummary />', () => {
// Act
// Assert
expect(getByText('Group by')).toBeInTheDocument();
expect(getByText('Aggregations')).toBeInTheDocument();
await wait();
await waitFor(() => {
expect(queryByText('Group by')).toBeInTheDocument();
expect(queryByText('Aggregations')).toBeInTheDocument();
});
});
});