From 23ff5a476723664781236990729cc133cef4a13c Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 23 Aug 2021 18:35:32 +0200 Subject: [PATCH] [DataViews] Fix excessive `resolve_index` requests in create data view flyout (#109500) --- .../index_pattern_editor_flyout_content.tsx | 159 ++++++++++++------ 1 file changed, 112 insertions(+), 47 deletions(-) diff --git a/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx b/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx index 14e1da44c7a35..982b0beaea616 100644 --- a/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx +++ b/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx @@ -9,6 +9,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import memoizeOne from 'memoize-one'; import { IndexPatternSpec, @@ -105,12 +106,22 @@ const IndexPatternEditorFlyoutContentComponent = ({ const { getFields } = form; - const [{ title, allowHidden, type }] = useFormData({ form }); + // `useFormData` initially returns `undefined`, + // we override `undefined` with real default values from `schema` + // to get a stable reference to avoid hooks re-run and reduce number of excessive requests + const [ + { + title = schema.title.defaultValue, + allowHidden = schema.allowHidden.defaultValue, + type = schema.type.defaultValue, + }, + ] = useFormData({ form }); const [isLoadingSources, setIsLoadingSources] = useState(true); const [timestampFieldOptions, setTimestampFieldOptions] = useState([]); const [isLoadingTimestampFields, setIsLoadingTimestampFields] = useState(false); const [isLoadingMatchedIndices, setIsLoadingMatchedIndices] = useState(false); + const currentLoadingMatchedIndicesRef = useRef(0); const [allSources, setAllSources] = useState([]); const [isLoadingIndexPatterns, setIsLoadingIndexPatterns] = useState(true); const [existingIndexPatterns, setExistingIndexPatterns] = useState([]); @@ -165,7 +176,9 @@ const IndexPatternEditorFlyoutContentComponent = ({ try { const response = await http.get('/api/rollup/indices'); if (isMounted.current) { - setRollupIndicesCapabilities(response || {}); + if (response) { + setRollupIndicesCapabilities(response); + } } } catch (e) { // Silently swallow failure responses such as expired trials @@ -225,52 +238,31 @@ const IndexPatternEditorFlyoutContentComponent = ({ let newRollupIndexName: string | undefined; const fetchIndices = async (query: string = '') => { - setIsLoadingMatchedIndices(true); - const indexRequests = []; - - if (query?.endsWith('*')) { - const exactMatchedQuery = getIndices({ - http, - isRollupIndex, - pattern: query, - showAllIndices: allowHidden, - searchClient, - }); - indexRequests.push(exactMatchedQuery); - // provide default value when not making a request for the partialMatchQuery - indexRequests.push(Promise.resolve([])); - } else { - const exactMatchQuery = getIndices({ - http, - isRollupIndex, - pattern: query, - showAllIndices: allowHidden, - searchClient, - }); - const partialMatchQuery = getIndices({ - http, - isRollupIndex, - pattern: `${query}*`, - showAllIndices: allowHidden, - searchClient, - }); - - indexRequests.push(exactMatchQuery); - indexRequests.push(partialMatchQuery); - } - - const [exactMatched, partialMatched] = (await ensureMinimumTime( - indexRequests - )) as MatchedItem[][]; + const currentLoadingMatchedIndicesIdx = ++currentLoadingMatchedIndicesRef.current; - const matchedIndicesResult = getMatchedIndices( - allSources, - partialMatched, - exactMatched, - allowHidden - ); + setIsLoadingMatchedIndices(true); - if (isMounted.current) { + const { matchedIndicesResult, exactMatched } = !isLoadingSources + ? await loadMatchedIndices(query, allowHidden, allSources, { + isRollupIndex, + http, + searchClient, + }) + : { + matchedIndicesResult: { + exactMatchedIndices: [], + allIndices: [], + partialMatchedIndices: [], + visibleIndices: [], + }, + exactMatched: [], + }; + + if ( + currentLoadingMatchedIndicesIdx === currentLoadingMatchedIndicesRef.current && + isMounted.current + ) { + // we are still interested in this result if (type === INDEX_PATTERN_TYPE.ROLLUP) { const rollupIndices = exactMatched.filter((index) => isRollupIndex(index.name)); newRollupIndexName = rollupIndices.length === 1 ? rollupIndices[0].name : undefined; @@ -288,7 +280,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ return fetchIndices(newTitle); }, - [http, allowHidden, allSources, type, rollupIndicesCapabilities, searchClient] + [http, allowHidden, allSources, type, rollupIndicesCapabilities, searchClient, isLoadingSources] ); useEffect(() => { @@ -396,3 +388,76 @@ const IndexPatternEditorFlyoutContentComponent = ({ }; export const IndexPatternEditorFlyoutContent = React.memo(IndexPatternEditorFlyoutContentComponent); + +// loadMatchedIndices is called both as an side effect inside of a parent component and the inside forms validation functions +// that are challenging to synchronize without a larger refactor +// Use memoizeOne as a caching layer to avoid excessive network requests on each key type +// TODO: refactor to remove `memoize` when https://github.com/elastic/kibana/pull/109238 is done +const loadMatchedIndices = memoizeOne( + async ( + query: string, + allowHidden: boolean, + allSources: MatchedItem[], + { + isRollupIndex, + http, + searchClient, + }: { + isRollupIndex: (index: string) => boolean; + http: IndexPatternEditorContext['http']; + searchClient: IndexPatternEditorContext['searchClient']; + } + ): Promise<{ + matchedIndicesResult: MatchedIndicesSet; + exactMatched: MatchedItem[]; + partialMatched: MatchedItem[]; + }> => { + const indexRequests = []; + + if (query?.endsWith('*')) { + const exactMatchedQuery = getIndices({ + http, + isRollupIndex, + pattern: query, + showAllIndices: allowHidden, + searchClient, + }); + indexRequests.push(exactMatchedQuery); + // provide default value when not making a request for the partialMatchQuery + indexRequests.push(Promise.resolve([])); + } else { + const exactMatchQuery = getIndices({ + http, + isRollupIndex, + pattern: query, + showAllIndices: allowHidden, + searchClient, + }); + const partialMatchQuery = getIndices({ + http, + isRollupIndex, + pattern: `${query}*`, + showAllIndices: allowHidden, + searchClient, + }); + + indexRequests.push(exactMatchQuery); + indexRequests.push(partialMatchQuery); + } + + const [exactMatched, partialMatched] = (await ensureMinimumTime( + indexRequests + )) as MatchedItem[][]; + + const matchedIndicesResult = getMatchedIndices( + allSources, + partialMatched, + exactMatched, + allowHidden + ); + + return { matchedIndicesResult, exactMatched, partialMatched }; + }, + // compare only query and allowHidden + (newArgs, oldArgs) => newArgs[0] === oldArgs[0] && newArgs[1] === oldArgs[1] +);