[ML] Fix unnecessary trigger of wildcard field type search for ML plugin routes. (#84605)

Passing in an empty string '' to useResolver() would trigger a wild card search across all indices and fields, potentially causing a timeout and the page would fail to load. The following pages were affected: Single Metric Viewer, Data frame analytics models list, Data frame analytics jobs list, Data frame analytics exploration page, File Data Visualizer (Data visualizer - Import data from a log file). This PR fixes it by passing undefined instead of '' to useResolver to avoid calling _fields_for_wildcard with an empty pattern. Jest tests were added to cover the two parameter scenarios empty string/undefined.
This commit is contained in:
Walter Rafelsberger 2020-12-01 18:05:57 +01:00 committed by GitHub
parent 5420177485
commit 5889e366da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 152 additions and 38 deletions

View file

@ -30,7 +30,7 @@ export function getDefaultDatafeedQuery() {
export function createSearchItems(
kibanaConfig: IUiSettingsClient,
indexPattern: IIndexPattern,
indexPattern: IIndexPattern | undefined,
savedSearch: SavedSearchSavedObject | null
) {
// query is only used by the data visualizer as it needs

View file

@ -38,7 +38,7 @@ export const analyticsJobExplorationRouteFactory = (
});
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
const { context } = useResolver('', undefined, deps.config, basicResolvers(deps));
const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps));
const [globalState] = useUrlState('_g');

View file

@ -34,7 +34,7 @@ export const analyticsJobsListRouteFactory = (
});
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
const { context } = useResolver('', undefined, deps.config, basicResolvers(deps));
const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps));
return (
<PageLoader context={context}>
<Page />

View file

@ -34,7 +34,7 @@ export const analyticsMapRouteFactory = (
});
const PageWrapper: FC<PageProps> = ({ deps }) => {
const { context } = useResolver('', undefined, deps.config, basicResolvers(deps));
const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps));
return (
<PageLoader context={context}>

View file

@ -34,7 +34,7 @@ export const modelsListRouteFactory = (
});
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
const { context } = useResolver('', undefined, deps.config, basicResolvers(deps));
const { context } = useResolver(undefined, undefined, deps.config, basicResolvers(deps));
return (
<PageLoader context={context}>
<Page />

View file

@ -45,7 +45,7 @@ export const fileBasedRouteFactory = (
const PageWrapper: FC<PageProps> = ({ location, deps }) => {
const { redirectToMlAccessDeniedPage } = deps;
const { context } = useResolver('', undefined, deps.config, {
const { context } = useResolver(undefined, undefined, deps.config, {
checkBasicLicense,
loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns),
checkFindFileStructurePrivilege: () =>

View file

@ -63,7 +63,7 @@ export const timeSeriesExplorerRouteFactory = (
});
const PageWrapper: FC<PageProps> = ({ deps }) => {
const { context, results } = useResolver('', undefined, deps.config, {
const { context, results } = useResolver(undefined, undefined, deps.config, {
...basicResolvers(deps),
jobs: mlJobService.loadJobsWrapper,
jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()),

View file

@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { IUiSettingsClient } from 'kibana/public';
import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url';
import { useNotifications } from '../contexts/kibana';
import { useResolver } from './use_resolver';
jest.mock('../contexts/kibana/use_create_url', () => {
return {
useCreateAndNavigateToMlLink: jest.fn(),
};
});
jest.mock('../contexts/kibana', () => {
return {
useMlUrlGenerator: () => ({
createUrl: jest.fn(),
}),
useNavigateToPath: () => jest.fn(),
useNotifications: jest.fn(),
};
});
const addError = jest.fn();
(useNotifications as jest.Mock).mockImplementation(() => ({
toasts: { addSuccess: jest.fn(), addDanger: jest.fn(), addError },
}));
const redirectToJobsManagementPage = jest.fn(() => Promise.resolve());
(useCreateAndNavigateToMlLink as jest.Mock).mockImplementation(() => redirectToJobsManagementPage);
describe('useResolver', () => {
afterEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.advanceTimersByTime(0);
jest.useRealTimers();
});
it('should accept undefined as indexPatternId and savedSearchId.', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useResolver(undefined, undefined, {} as IUiSettingsClient, {})
);
await act(async () => {
await waitForNextUpdate();
});
expect(result.current).toStrictEqual({
context: {
combinedQuery: {
bool: {
must: [
{
match_all: {},
},
],
},
},
currentIndexPattern: null,
currentSavedSearch: null,
indexPatterns: null,
kibanaConfig: {},
},
results: {},
});
expect(addError).toHaveBeenCalledTimes(0);
expect(redirectToJobsManagementPage).toHaveBeenCalledTimes(0);
});
it('should add an error toast and redirect if indexPatternId is an empty string.', async () => {
const { result } = renderHook(() => useResolver('', undefined, {} as IUiSettingsClient, {}));
await act(async () => {});
expect(result.current).toStrictEqual({ context: null, results: {} });
expect(addError).toHaveBeenCalledTimes(1);
expect(redirectToJobsManagementPage).toHaveBeenCalledTimes(1);
});
});

View file

@ -11,6 +11,7 @@ import {
getIndexPatternById,
getIndexPatternsContract,
getIndexPatternAndSavedSearch,
IndexPatternAndSavedSearch,
} from '../util/index_utils';
import { createSearchItems } from '../jobs/new_job/utils/new_job_utils';
import { ResolverResults, Resolvers } from './resolvers';
@ -19,6 +20,14 @@ import { useNotifications } from '../contexts/kibana';
import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url';
import { ML_PAGES } from '../../../common/constants/ml_url_generator';
/**
* Hook to resolve route specific requirements
* @param indexPatternId optional Kibana index pattern id, used for wizards
* @param savedSearchId optional Kibana saved search id, used for wizards
* @param config Kibana UI Settings
* @param resolvers an array of resolvers to be executed for the route
* @return { context, results } returns the ML context and resolver results
*/
export const useResolver = (
indexPatternId: string | undefined,
savedSearchId: string | undefined,
@ -52,36 +61,49 @@ export const useResolver = (
return;
}
if (indexPatternId !== undefined || savedSearchId !== undefined) {
try {
// note, currently we're using our own kibana context that requires a current index pattern to be set
// this means, if the page uses this context, useResolver must be passed a string for the index pattern id
// and loadIndexPatterns must be part of the resolvers.
const { indexPattern, savedSearch } =
savedSearchId !== undefined
? await getIndexPatternAndSavedSearch(savedSearchId)
: { savedSearch: null, indexPattern: await getIndexPatternById(indexPatternId!) };
const { combinedQuery } = createSearchItems(config, indexPattern!, savedSearch);
setContext({
combinedQuery,
currentIndexPattern: indexPattern,
currentSavedSearch: savedSearch,
indexPatterns: getIndexPatternsContract()!,
kibanaConfig: config,
});
} catch (error) {
// an unexpected error has occurred. This could be caused by an incorrect index pattern or saved search ID
notifications.toasts.addError(new Error(error), {
title: i18n.translate('xpack.ml.useResolver.errorTitle', {
defaultMessage: 'An error has occurred',
}),
});
await redirectToJobsManagementPage();
try {
if (indexPatternId === '') {
throw new Error(
i18n.translate('xpack.ml.useResolver.errorIndexPatternIdEmptyString', {
defaultMessage: 'indexPatternId must not be empty string.',
})
);
}
} else {
setContext({});
let indexPatternAndSavedSearch: IndexPatternAndSavedSearch = {
savedSearch: null,
indexPattern: null,
};
if (savedSearchId !== undefined) {
indexPatternAndSavedSearch = await getIndexPatternAndSavedSearch(savedSearchId);
} else if (indexPatternId !== undefined) {
indexPatternAndSavedSearch.indexPattern = await getIndexPatternById(indexPatternId);
}
const { savedSearch, indexPattern } = indexPatternAndSavedSearch;
const { combinedQuery } = createSearchItems(
config,
indexPattern !== null ? indexPattern : undefined,
savedSearch
);
setContext({
combinedQuery,
currentIndexPattern: indexPattern,
currentSavedSearch: savedSearch,
indexPatterns: getIndexPatternsContract(),
kibanaConfig: config,
});
} catch (error) {
// an unexpected error has occurred. This could be caused by an incorrect index pattern or saved search ID
notifications.toasts.addError(new Error(error), {
title: i18n.translate('xpack.ml.useResolver.errorTitle', {
defaultMessage: 'An error has occurred',
}),
});
await redirectToJobsManagementPage();
}
})();
}, []);

View file

@ -73,9 +73,12 @@ export function getIndexPatternIdFromName(name: string) {
}
return null;
}
export interface IndexPatternAndSavedSearch {
savedSearch: SavedSearchSavedObject | null;
indexPattern: IIndexPattern | null;
}
export async function getIndexPatternAndSavedSearch(savedSearchId: string) {
const resp: { savedSearch: SavedSearchSavedObject | null; indexPattern: IIndexPattern | null } = {
const resp: IndexPatternAndSavedSearch = {
savedSearch: null,
indexPattern: null,
};