[Enterprise Search] Add filtering for search indices (#154720)

## Summary

Adds a toggle to only show search-optimized indices to our indices page.

Also splits up the fetch indices calls for our search applications and
the indices page into two separate flows.

<img width="1144" alt="Screenshot 2023-04-11 at 14 30 08"
src="https://user-images.githubusercontent.com/94373878/231163183-21b58d58-e28e-44f2-a42e-033508e15ebd.png">
This commit is contained in:
Sander Philipse 2023-04-12 16:42:25 +02:00 committed by GitHub
parent bada04c27b
commit 43fa6e549c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 758 additions and 604 deletions

View file

@ -21,15 +21,24 @@ describe('FetchIndicesApiLogic', () => {
const promise = Promise.resolve({ result: 'result' });
http.get.mockReturnValue(promise);
const result = fetchIndices({
meta: { page: { current: 1, size: 20, total_pages: 10, total_results: 10 } },
from: 0,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
size: 20,
});
await nextTick();
expect(http.get).toHaveBeenCalledWith('/internal/enterprise_search/indices', {
query: { page: 1, return_hidden_indices: false, search_query: null, size: 20 },
query: {
from: 0,
only_show_search_optimized_indices: false,
return_hidden_indices: false,
search_query: null,
size: 20,
},
});
await expect(result).resolves.toEqual({
isInitialRequest: true,
onlyShowSearchOptimizedIndices: false,
result: 'result',
returnHiddenIndices: false,
searchQuery: undefined,
@ -39,15 +48,24 @@ describe('FetchIndicesApiLogic', () => {
const promise = Promise.resolve({ result: 'result' });
http.get.mockReturnValue(promise);
const result = fetchIndices({
meta: { page: { current: 2, size: 20, total_pages: 10, total_results: 10 } },
from: 1,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
size: 20,
});
await nextTick();
expect(http.get).toHaveBeenCalledWith('/internal/enterprise_search/indices', {
query: { page: 2, return_hidden_indices: false, search_query: null, size: 20 },
query: {
from: 1,
only_show_search_optimized_indices: false,
return_hidden_indices: false,
search_query: null,
size: 20,
},
});
await expect(result).resolves.toEqual({
isInitialRequest: false,
onlyShowSearchOptimizedIndices: false,
result: 'result',
returnHiddenIndices: false,
searchQuery: undefined,
@ -57,16 +75,25 @@ describe('FetchIndicesApiLogic', () => {
const promise = Promise.resolve({ result: 'result' });
http.get.mockReturnValue(promise);
const result = fetchIndices({
meta: { page: { current: 1, size: 20, total_pages: 10, total_results: 10 } },
from: 0,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
searchQuery: 'a',
size: 20,
});
await nextTick();
expect(http.get).toHaveBeenCalledWith('/internal/enterprise_search/indices', {
query: { page: 1, return_hidden_indices: false, search_query: 'a', size: 20 },
query: {
from: 0,
only_show_search_optimized_indices: false,
return_hidden_indices: false,
search_query: 'a',
size: 20,
},
});
await expect(result).resolves.toEqual({
isInitialRequest: false,
onlyShowSearchOptimizedIndices: false,
result: 'result',
returnHiddenIndices: false,
searchQuery: 'a',

View file

@ -5,28 +5,44 @@
* 2.0.
*/
import { Meta } from '../../../../../common/types';
import { ElasticsearchIndexWithIngestion } from '../../../../../common/types/indices';
import { Meta } from '../../../../../common/types/pagination';
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';
export const fetchIndices = async ({
meta,
returnHiddenIndices,
searchQuery,
}: {
meta: Meta;
export interface FetchIndicesParams {
from: number;
onlyShowSearchOptimizedIndices: boolean;
returnHiddenIndices: boolean;
searchQuery?: string;
}) => {
size?: number;
}
export interface FetchIndicesResponse {
indices: ElasticsearchIndexWithIngestion[];
isInitialRequest: boolean;
meta: Meta;
onlyShowSearchOptimizedIndices: boolean;
returnHiddenIndices: boolean;
searchQuery?: string;
}
export const fetchIndices = async ({
from,
onlyShowSearchOptimizedIndices,
returnHiddenIndices,
searchQuery,
size,
}: FetchIndicesParams): Promise<FetchIndicesResponse> => {
const { http } = HttpLogic.values;
const route = '/internal/enterprise_search/indices';
const query = {
page: meta.page.current,
from,
only_show_search_optimized_indices: onlyShowSearchOptimizedIndices,
return_hidden_indices: returnHiddenIndices,
search_query: searchQuery || null,
size: 20,
size: size ?? 20,
};
const response = await http.get<{ indices: ElasticsearchIndexWithIngestion[]; meta: Meta }>(
route,
@ -36,9 +52,17 @@ export const fetchIndices = async ({
);
// We need this to determine whether to show the empty state on the indices page
const isInitialRequest = meta.page.current === 1 && !searchQuery;
const isInitialRequest = from === 0 && !searchQuery && !onlyShowSearchOptimizedIndices;
return { ...response, isInitialRequest, returnHiddenIndices, searchQuery };
return {
...response,
isInitialRequest,
onlyShowSearchOptimizedIndices,
returnHiddenIndices,
searchQuery,
};
};
export const FetchIndicesAPILogic = createApiLogic(['content', 'indices_api_logic'], fetchIndices);
export type FetchIndicesApiActions = Actions<FetchIndicesParams, FetchIndicesResponse>;

View file

@ -7,8 +7,6 @@
import { LogicMounter, mockFlashMessageHelpers } from '../../../__mocks__/kea_logic';
import { indices } from '../../__mocks__/search_indices.mock';
import { connectorIndex, elasticsearchViewIndices } from '../../__mocks__/view_index.mock';
import moment from 'moment';
@ -18,7 +16,6 @@ import { nextTick } from '@kbn/test-jest-helpers';
import { HttpError, Status } from '../../../../../common/types/api';
import { ConnectorStatus, SyncStatus } from '../../../../../common/types/connectors';
import { DEFAULT_META } from '../../../shared/constants';
import { FetchIndicesAPILogic } from '../../api/index/fetch_indices_api_logic';
@ -26,6 +23,22 @@ import { IngestionMethod, IngestionStatus } from '../../types';
import { IndicesLogic } from './indices_logic';
const DEFAULT_META = {
page: {
from: 0,
size: 20,
total: 20,
},
};
const EMPTY_META = {
page: {
from: 0,
size: 20,
total: 0,
},
};
const DEFAULT_VALUES = {
data: undefined,
deleteModalIndex: null,
@ -42,8 +55,13 @@ const DEFAULT_VALUES = {
isFetchIndexDetailsLoading: true,
isFirstRequest: true,
isLoading: true,
meta: DEFAULT_META,
searchParams: { meta: DEFAULT_META, returnHiddenIndices: false },
meta: EMPTY_META,
searchParams: {
from: 0,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
size: 20,
},
status: Status.IDLE,
};
@ -69,15 +87,9 @@ describe('IndicesLogic', () => {
IndicesLogic.actions.onPaginate(3);
expect(IndicesLogic.values).toEqual({
...DEFAULT_VALUES,
meta: {
page: {
...DEFAULT_META.page,
current: 3,
},
},
searchParams: {
...DEFAULT_VALUES.searchParams,
meta: { page: { ...DEFAULT_META.page, current: 3 } },
from: 40,
},
});
});
@ -132,7 +144,8 @@ describe('IndicesLogic', () => {
IndicesLogic.actions.apiSuccess({
indices: [],
isInitialRequest: false,
meta: DEFAULT_VALUES.meta,
meta: DEFAULT_META,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
});
@ -141,54 +154,15 @@ describe('IndicesLogic', () => {
data: {
indices: [],
isInitialRequest: false,
meta: DEFAULT_VALUES.meta,
meta: DEFAULT_META,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
},
hasNoIndices: false,
indices: [],
isFirstRequest: false,
isLoading: false,
status: Status.SUCCESS,
});
});
});
describe('meta', () => {
it('updates when apiSuccess listener triggered', () => {
const newMeta = {
page: {
current: 2,
size: 5,
total_pages: 10,
total_results: 52,
},
};
expect(IndicesLogic.values).toEqual(DEFAULT_VALUES);
IndicesLogic.actions.apiSuccess({
indices,
isInitialRequest: true,
meta: newMeta,
returnHiddenIndices: true,
searchQuery: 'a',
});
expect(IndicesLogic.values).toEqual({
...DEFAULT_VALUES,
data: {
indices,
isInitialRequest: true,
meta: newMeta,
returnHiddenIndices: true,
searchQuery: 'a',
},
hasNoIndices: false,
indices: elasticsearchViewIndices,
isFirstRequest: false,
isLoading: false,
meta: newMeta,
searchParams: {
meta: newMeta,
returnHiddenIndices: true,
searchQuery: 'a',
},
meta: DEFAULT_META,
status: Status.SUCCESS,
});
});
@ -197,10 +171,9 @@ describe('IndicesLogic', () => {
it('updates to true when apiSuccess returns initialRequest: true with no indices', () => {
const meta = {
page: {
current: 1,
from: 0,
size: 0,
total_pages: 1,
total_results: 0,
total: 0,
},
};
expect(IndicesLogic.values).toEqual(DEFAULT_VALUES);
@ -208,6 +181,7 @@ describe('IndicesLogic', () => {
indices: [],
isInitialRequest: true,
meta,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
});
expect(IndicesLogic.values).toEqual({
@ -216,6 +190,7 @@ describe('IndicesLogic', () => {
indices: [],
isInitialRequest: true,
meta,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
},
hasNoIndices: true,
@ -225,7 +200,8 @@ describe('IndicesLogic', () => {
meta,
searchParams: {
...DEFAULT_VALUES.searchParams,
meta,
from: 0,
size: 0,
},
status: Status.SUCCESS,
});
@ -233,10 +209,9 @@ describe('IndicesLogic', () => {
it('updates to false when apiSuccess returns initialRequest: false with no indices', () => {
const meta = {
page: {
current: 1,
from: 0,
size: 0,
total_pages: 1,
total_results: 0,
total: 0,
},
};
expect(IndicesLogic.values).toEqual(DEFAULT_VALUES);
@ -244,6 +219,7 @@ describe('IndicesLogic', () => {
indices: [],
isInitialRequest: false,
meta,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
});
expect(IndicesLogic.values).toEqual({
@ -252,6 +228,7 @@ describe('IndicesLogic', () => {
indices: [],
isInitialRequest: false,
meta,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
},
hasNoIndices: false,
@ -261,7 +238,8 @@ describe('IndicesLogic', () => {
meta,
searchParams: {
...DEFAULT_VALUES.searchParams,
meta,
from: 0,
size: 0,
},
status: Status.SUCCESS,
});
@ -301,7 +279,12 @@ describe('IndicesLogic', () => {
describe('listeners', () => {
it('calls clearFlashMessages on new makeRequest', () => {
IndicesLogic.actions.makeRequest({ meta: DEFAULT_META, returnHiddenIndices: false });
IndicesLogic.actions.makeRequest({
from: 0,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
size: 20,
});
expect(mockFlashMessageHelpers.clearFlashMessages).toHaveBeenCalledTimes(1);
});
it('calls flashSuccessToast, closeDeleteModal and fetchIndices on deleteSuccess', () => {
@ -317,41 +300,72 @@ describe('IndicesLogic', () => {
it('calls makeRequest on fetchIndices', async () => {
jest.useFakeTimers({ legacyFakeTimers: true });
IndicesLogic.actions.makeRequest = jest.fn();
IndicesLogic.actions.fetchIndices({ meta: DEFAULT_META, returnHiddenIndices: false });
IndicesLogic.actions.fetchIndices({
from: 0,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
size: 20,
});
jest.advanceTimersByTime(150);
await nextTick();
expect(IndicesLogic.actions.makeRequest).toHaveBeenCalledWith({
meta: DEFAULT_META,
from: 0,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
size: 20,
});
});
it('calls makeRequest once on two fetchIndices calls within 150ms', async () => {
jest.useFakeTimers({ legacyFakeTimers: true });
IndicesLogic.actions.makeRequest = jest.fn();
IndicesLogic.actions.fetchIndices({ meta: DEFAULT_META, returnHiddenIndices: false });
IndicesLogic.actions.fetchIndices({
from: 0,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
size: 20,
});
jest.advanceTimersByTime(130);
await nextTick();
IndicesLogic.actions.fetchIndices({ meta: DEFAULT_META, returnHiddenIndices: false });
IndicesLogic.actions.fetchIndices({
from: 0,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
size: 20,
});
jest.advanceTimersByTime(150);
await nextTick();
expect(IndicesLogic.actions.makeRequest).toHaveBeenCalledWith({
meta: DEFAULT_META,
from: 0,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
size: 20,
});
expect(IndicesLogic.actions.makeRequest).toHaveBeenCalledTimes(1);
});
it('calls makeRequest twice on two fetchIndices calls outside 150ms', async () => {
jest.useFakeTimers({ legacyFakeTimers: true });
IndicesLogic.actions.makeRequest = jest.fn();
IndicesLogic.actions.fetchIndices({ meta: DEFAULT_META, returnHiddenIndices: false });
IndicesLogic.actions.fetchIndices({
from: 0,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
size: 20,
});
jest.advanceTimersByTime(150);
await nextTick();
IndicesLogic.actions.fetchIndices({ meta: DEFAULT_META, returnHiddenIndices: false });
IndicesLogic.actions.fetchIndices({
from: 0,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
size: 20,
});
jest.advanceTimersByTime(150);
await nextTick();
expect(IndicesLogic.actions.makeRequest).toHaveBeenCalledWith({
meta: DEFAULT_META,
from: 0,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
size: 20,
});
expect(IndicesLogic.actions.makeRequest).toHaveBeenCalledTimes(2);
});
@ -365,6 +379,7 @@ describe('IndicesLogic', () => {
indices: elasticsearchViewIndices,
isInitialRequest: true,
meta: DEFAULT_META,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
});
@ -374,6 +389,7 @@ describe('IndicesLogic', () => {
indices: elasticsearchViewIndices,
isInitialRequest: true,
meta: DEFAULT_META,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
},
hasNoIndices: false,
@ -401,6 +417,7 @@ describe('IndicesLogic', () => {
],
isInitialRequest: true,
meta: DEFAULT_META,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
});
@ -419,6 +436,7 @@ describe('IndicesLogic', () => {
],
isInitialRequest: true,
meta: DEFAULT_META,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
},
hasNoIndices: false,
@ -450,6 +468,7 @@ describe('IndicesLogic', () => {
],
isInitialRequest: true,
meta: DEFAULT_META,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
});
@ -464,6 +483,7 @@ describe('IndicesLogic', () => {
],
isInitialRequest: true,
meta: DEFAULT_META,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
},
hasNoIndices: false,
@ -494,6 +514,7 @@ describe('IndicesLogic', () => {
],
isInitialRequest: true,
meta: DEFAULT_META,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
});
@ -508,6 +529,7 @@ describe('IndicesLogic', () => {
],
isInitialRequest: true,
meta: DEFAULT_META,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
},
hasNoIndices: false,
@ -539,6 +561,7 @@ describe('IndicesLogic', () => {
],
isInitialRequest: true,
meta: DEFAULT_META,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
});
@ -557,6 +580,7 @@ describe('IndicesLogic', () => {
],
isInitialRequest: true,
meta: DEFAULT_META,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
},
hasNoIndices: false,

View file

@ -7,12 +7,9 @@
import { kea, MakeLogicType } from 'kea';
import { Meta } from '../../../../../common/types';
import { HttpError, Status } from '../../../../../common/types/api';
import { ElasticsearchIndexWithIngestion } from '../../../../../common/types/indices';
import { Status } from '../../../../../common/types/api';
import { Meta } from '../../../../../common/types/pagination';
import { Actions } from '../../../shared/api_logic/create_api_logic';
import { DEFAULT_META } from '../../../shared/constants';
import { updateMetaPageIndex } from '../../../shared/table_pagination';
import {
CancelSyncsActions,
CancelSyncsApiLogic,
@ -27,31 +24,16 @@ import {
FetchIndexApiLogic,
FetchIndexApiResponse,
} from '../../api/index/fetch_index_api_logic';
import { FetchIndicesAPILogic } from '../../api/index/fetch_indices_api_logic';
import {
FetchIndicesApiActions,
FetchIndicesAPILogic,
} from '../../api/index/fetch_indices_api_logic';
import { ElasticsearchViewIndex, IngestionMethod } from '../../types';
import { getIngestionMethod, indexToViewIndex } from '../../utils/indices';
export interface IndicesActions {
apiError(error: HttpError): HttpError;
apiSuccess({
indices,
isInitialRequest,
meta,
returnHiddenIndices,
searchQuery,
}: {
indices: ElasticsearchIndexWithIngestion[];
isInitialRequest: boolean;
meta: Meta;
returnHiddenIndices: boolean;
searchQuery?: string;
}): {
indices: ElasticsearchIndexWithIngestion[];
isInitialRequest: boolean;
meta: Meta;
returnHiddenIndices: boolean;
searchQuery?: string;
};
apiError: FetchIndicesApiActions['apiError'];
apiSuccess: FetchIndicesApiActions['apiSuccess'];
cancelSuccess: CancelSyncsActions['apiSuccess'];
closeDeleteModal(): void;
deleteError: Actions<DeleteIndexApiLogicArgs, DeleteIndexApiLogicValues>['apiError'];
@ -59,15 +41,26 @@ export interface IndicesActions {
deleteSuccess: Actions<DeleteIndexApiLogicArgs, DeleteIndexApiLogicValues>['apiSuccess'];
fetchIndexDetails: FetchIndexActions['makeRequest'];
fetchIndices({
meta,
from,
onlyShowSearchOptimizedIndices,
returnHiddenIndices,
searchQuery,
size,
}: {
meta: Meta;
from: number;
onlyShowSearchOptimizedIndices: boolean;
returnHiddenIndices: boolean;
searchQuery?: string;
}): { meta: Meta; returnHiddenIndices: boolean; searchQuery?: string };
makeRequest: typeof FetchIndicesAPILogic.actions.makeRequest;
size: number;
}): {
from: number;
meta: Meta;
onlyShowSearchOptimizedIndices: boolean;
returnHiddenIndices: boolean;
searchQuery?: string;
size: number;
};
makeRequest: FetchIndicesApiActions['makeRequest'];
onPaginate(newPageIndex: number): { newPageIndex: number };
openDeleteModal(indexName: string): { indexName: string };
setIsFirstRequest(): void;
@ -89,17 +82,31 @@ export interface IndicesValues {
isFirstRequest: boolean;
isLoading: boolean;
meta: Meta;
searchParams: { meta: Meta; returnHiddenIndices: boolean; searchQuery?: string };
searchParams: {
from: number;
onlyShowSearchOptimizedIndices: boolean;
returnHiddenIndices: boolean;
searchQuery?: string;
size: number;
};
status: typeof FetchIndicesAPILogic.values.status;
}
export const IndicesLogic = kea<MakeLogicType<IndicesValues, IndicesActions>>({
actions: {
closeDeleteModal: true,
fetchIndices: ({ meta, returnHiddenIndices, searchQuery }) => ({
meta,
fetchIndices: ({
from,
onlyShowSearchOptimizedIndices,
returnHiddenIndices,
searchQuery,
size,
}) => ({
from,
onlyShowSearchOptimizedIndices,
returnHiddenIndices,
searchQuery,
size,
}),
onPaginate: (newPageIndex) => ({ newPageIndex }),
openDeleteModal: (indexName) => ({ indexName }),
@ -166,16 +173,26 @@ export const IndicesLogic = kea<MakeLogicType<IndicesValues, IndicesActions>>({
},
],
searchParams: [
{ meta: DEFAULT_META, returnHiddenIndices: false },
{
apiSuccess: (_, { meta, returnHiddenIndices, searchQuery }) => ({
meta,
from: 0,
onlyShowSearchOptimizedIndices: false,
returnHiddenIndices: false,
size: 20,
},
{
apiSuccess: (
_,
{ meta, onlyShowSearchOptimizedIndices, returnHiddenIndices, searchQuery }
) => ({
from: meta.page.from,
onlyShowSearchOptimizedIndices,
returnHiddenIndices,
searchQuery,
size: meta.page.size,
}),
onPaginate: (state, { newPageIndex }) => ({
...state,
meta: updateMetaPageIndex(state.meta, newPageIndex),
from: (newPageIndex - 1) * state.size,
}),
},
],
@ -218,6 +235,9 @@ export const IndicesLogic = kea<MakeLogicType<IndicesValues, IndicesActions>>({
() => [selectors.status, selectors.isFirstRequest],
(status, isFirstRequest) => [Status.LOADING, Status.IDLE].includes(status) && isFirstRequest,
],
meta: [() => [selectors.searchParams], (searchParams) => searchParams.meta],
meta: [
() => [selectors.data],
(data) => data?.meta ?? { page: { from: 0, size: 20, total: 0 } },
],
}),
});

View file

@ -21,13 +21,12 @@ import { i18n } from '@kbn/i18n';
import { NATIVE_CONNECTOR_DEFINITIONS } from '../../../../../common/connectors/native_connectors';
import { Meta } from '../../../../../common/types';
import { Meta } from '../../../../../common/types/pagination';
import { healthColorsMap } from '../../../shared/constants/health_colors';
import { generateEncodedPath } from '../../../shared/encode_path_params';
import { KibanaLogic } from '../../../shared/kibana';
import { EuiLinkTo } from '../../../shared/react_router_helpers';
import { EuiBadgeTo } from '../../../shared/react_router_helpers/eui_components';
import { convertMetaToPagination } from '../../../shared/table_pagination';
import { SEARCH_INDEX_PATH } from '../../routes';
import { ElasticsearchViewIndex } from '../../types';
import { ingestionMethodToText, isConnectorIndex } from '../../utils/indices';
@ -211,7 +210,12 @@ export const IndicesTable: React.FC<IndicesTableProps> = ({
items={indices}
columns={columns}
onChange={onChange}
pagination={{ ...convertMetaToPagination(meta), showPerPageOptions: false }}
pagination={{
pageIndex: meta.page.from / (meta.page.size || 1),
pageSize: meta.page.size,
showPerPageOptions: false,
totalItemCount: meta.page.total,
}}
tableLayout="fixed"
loading={isLoading}
/>

View file

@ -17,7 +17,6 @@ import { shallow } from 'enzyme';
import { EuiCallOut, EuiButton } from '@elastic/eui';
import { AddContentEmptyPrompt } from '../../../shared/add_content_empty_prompt';
import { DEFAULT_META } from '../../../shared/constants';
import { ElasticsearchResources } from '../../../shared/elasticsearch_resources';
import { GettingStartedSteps } from '../../../shared/getting_started_steps';
@ -27,7 +26,10 @@ import { SearchIndices } from './search_indices';
const mockValues = {
indices,
meta: DEFAULT_META,
searchParams: {
from: 0,
size: 20,
},
};
const mockActions = {

View file

@ -19,6 +19,8 @@ import {
EuiSwitch,
EuiSearchBar,
EuiLink,
EuiToolTip,
EuiCode,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -49,8 +51,9 @@ export const baseBreadcrumbs = [
export const SearchIndices: React.FC = () => {
const { fetchIndices, onPaginate, openDeleteModal, setIsFirstRequest } = useActions(IndicesLogic);
const { meta, indices, hasNoIndices, isLoading } = useValues(IndicesLogic);
const { meta, indices, hasNoIndices, isLoading, searchParams } = useValues(IndicesLogic);
const [showHiddenIndices, setShowHiddenIndices] = useState(false);
const [onlyShowSearchOptimizedIndices, setOnlyShowSearchOptimizedIndices] = useState(false);
const [searchQuery, setSearchValue] = useState('');
const [calloutDismissed, setCalloutDismissed] = useLocalStorage<boolean>(
@ -66,11 +69,19 @@ export const SearchIndices: React.FC = () => {
useEffect(() => {
fetchIndices({
meta,
from: searchParams.from,
onlyShowSearchOptimizedIndices,
returnHiddenIndices: showHiddenIndices,
searchQuery,
size: searchParams.size,
});
}, [searchQuery, meta.page.current, showHiddenIndices]);
}, [
searchQuery,
searchParams.from,
searchParams.size,
onlyShowSearchOptimizedIndices,
showHiddenIndices,
]);
const pageTitle = isLoading
? ''
@ -180,6 +191,30 @@ export const SearchIndices: React.FC = () => {
onChange={(event) => setShowHiddenIndices(event.target.checked)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
content={
<FormattedMessage
id="xpack.enterpriseSearch.content.searchIndices.searchIndices.onlySearchOptimized.tooltipContent"
defaultMessage="Search-optimized indices are prefixed with {code}. They are managed by ingestion mechanisms such as crawlers, connectors or ingestion APIs."
values={{ code: <EuiCode>search-</EuiCode> }}
/>
}
>
<EuiSwitch
checked={onlyShowSearchOptimizedIndices}
label={i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.searchIndices.onlySearchOptimized.label',
{
defaultMessage: 'Only show search-optimized indices',
}
)}
onChange={(event) =>
setOnlyShowSearchOptimizedIndices(event.target.checked)
}
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem className="entSearchIndicesSearchBar">
<EuiSearchBar
query={searchQuery}

View file

@ -0,0 +1,20 @@
/*
* 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 { IScopedClusterClient } from '@kbn/core/server';
import { CONNECTORS_INDEX } from '../..';
export async function fetchConnectorIndexNames(client: IScopedClusterClient): Promise<string[]> {
const result = await client.asCurrentUser.search({
_source: false,
fields: [{ field: 'index_name' }],
index: CONNECTORS_INDEX,
size: 10000,
});
return (result?.hits.hits ?? []).map((field) => field.fields?.index_name[0] ?? '');
}

View file

@ -15,9 +15,9 @@ import {
import { ByteSizeValue } from '@kbn/config-schema';
import { IScopedClusterClient } from '@kbn/core/server';
import { fetchIndices } from './fetch_indices';
import { fetchIndices, fetchSearchIndices } from './fetch_indices';
describe('fetchIndices lib function', () => {
describe('fetch indices lib functions', () => {
const mockClient = {
asCurrentUser: {
count: jest.fn().mockReturnValue({ count: 100 }),
@ -58,441 +58,308 @@ describe('fetchIndices lib function', () => {
},
};
mockClient.asCurrentUser.security.hasPrivileges.mockImplementation(() => ({
index: {
'index-without-prefix': { manage: true, read: true },
'search-aliased': { manage: true, read: true },
'search-double-aliased': { manage: true, read: true },
'search-regular-index': { manage: true, read: true },
'second-index': { manage: true, read: true },
},
}));
beforeEach(() => {
jest.clearAllMocks();
});
it('should return regular index without aliases', async () => {
mockClient.asCurrentUser.indices.get.mockImplementation(() => ({
...regularIndexResponse,
hidden: { aliases: {}, settings: { index: { hidden: 'true' } } },
mockClient.asCurrentUser.security.hasPrivileges.mockImplementation(() => ({
index: {
'index-without-prefix': { manage: true, read: true },
'search-aliased': { manage: true, read: true },
'search-double-aliased': { manage: true, read: true },
'search-regular-index': { manage: true, read: true },
'second-index': { manage: true, read: true },
},
}));
mockClient.asCurrentUser.indices.stats.mockImplementation(() => regularIndexStatsResponse);
});
await expect(
fetchIndices(mockClient as unknown as IScopedClusterClient, 'search-*', false, true)
).resolves.toEqual([
{
alias: false,
count: 100,
health: 'green',
hidden: false,
name: 'search-regular-index',
privileges: { manage: true, read: true },
status: 'open',
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
describe('fetchSearchIndices', () => {
it('should return index with unique aliases', async () => {
const aliasedIndexResponse = {
'index-without-prefix': {
...regularIndexResponse['search-regular-index'],
aliases: {
'search-aliased': {},
'search-double-aliased': {},
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
]);
expect(mockClient.asCurrentUser.indices.get).toHaveBeenCalledWith({
expand_wildcards: ['open'],
features: ['aliases', 'settings'],
filter_path: ['*.aliases', '*.settings.index.hidden'],
index: 'search-*',
});
'second-index': {
...regularIndexResponse['search-regular-index'],
aliases: {
'search-aliased': {},
},
},
};
const aliasedStatsResponse = {
indices: {
'index-without-prefix': { ...regularIndexStatsResponse.indices['search-regular-index'] },
'second-index': { ...regularIndexStatsResponse.indices['search-regular-index'] },
},
};
expect(mockClient.asCurrentUser.indices.stats).toHaveBeenCalledWith({
expand_wildcards: ['open'],
index: 'search-*',
metric: ['docs', 'store'],
});
expect(mockClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({
index: [
mockClient.asCurrentUser.indices.get.mockImplementationOnce(() => aliasedIndexResponse);
mockClient.asCurrentUser.indices.stats.mockImplementationOnce(() => aliasedStatsResponse);
await expect(
fetchSearchIndices(mockClient as unknown as IScopedClusterClient, {
alias_pattern: 'search-',
index_pattern: '.ent-search-engine-documents',
})
).resolves.toEqual([
{
names: ['search-regular-index', 'hidden'],
privileges: ['read', 'manage'],
count: 100,
health: 'green',
hidden: false,
name: 'index-without-prefix',
status: 'open',
alias: false,
privileges: { read: true, manage: true },
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
],
{
count: 100,
health: 'green',
hidden: false,
name: 'search-aliased',
status: 'open',
alias: true,
privileges: { read: true, manage: true },
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
{
count: 100,
health: 'green',
hidden: false,
name: 'search-double-aliased',
status: 'open',
alias: true,
privileges: { read: true, manage: true },
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
{
count: 100,
health: 'green',
hidden: false,
name: 'second-index',
status: 'open',
alias: false,
privileges: { read: true, manage: true },
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
]);
});
});
describe('alwaysShowPattern', () => {
const sortIndices = (index1: any, index2: any) => {
if (index1.name < index2.name) return -1;
if (index1.name > index2.name) return 1;
return 0;
};
it('should return hidden indices without aliases if specified', async () => {
mockClient.asCurrentUser.indices.get.mockImplementation(() => regularIndexResponse);
mockClient.asCurrentUser.indices.stats.mockImplementation(() => regularIndexStatsResponse);
it('overrides hidden indices setting', async () => {
mockClient.asCurrentUser.indices.get.mockImplementation(() => mockMultiIndexResponse);
mockClient.asCurrentUser.indices.stats.mockImplementation(() => mockMultiStatsResponse);
await expect(
fetchIndices(mockClient as unknown as IScopedClusterClient, 'search-*', true, true)
).resolves.toEqual([
{
alias: false,
count: 100,
health: 'green',
hidden: false,
name: 'search-regular-index',
privileges: { manage: true, read: true },
status: 'open',
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
]);
expect(mockClient.asCurrentUser.indices.get).toHaveBeenCalledWith({
expand_wildcards: ['hidden', 'all'],
features: ['aliases', 'settings'],
filter_path: ['*.aliases', '*.settings.index.hidden'],
index: 'search-*',
});
mockClient.asCurrentUser.security.hasPrivileges.mockImplementation(() => ({
index: mockPrivilegesResponse,
}));
const returnValue = await fetchSearchIndices(
mockClient as unknown as IScopedClusterClient,
{ alias_pattern: 'search-', index_pattern: '.ent-search-engine-documents' }
);
expect(mockClient.asCurrentUser.indices.stats).toHaveBeenCalledWith({
expand_wildcards: ['hidden', 'all'],
index: 'search-*',
metric: ['docs', 'store'],
});
});
it('should return index with unique aliases', async () => {
const aliasedIndexResponse = {
'index-without-prefix': {
...regularIndexResponse['search-regular-index'],
aliases: {
'search-aliased': {},
'search-double-aliased': {},
},
},
'second-index': {
...regularIndexResponse['search-regular-index'],
aliases: {
'search-aliased': {},
},
},
};
const aliasedStatsResponse = {
indices: {
'index-without-prefix': { ...regularIndexStatsResponse.indices['search-regular-index'] },
'second-index': { ...regularIndexStatsResponse.indices['search-regular-index'] },
},
};
mockClient.asCurrentUser.indices.get.mockImplementationOnce(() => aliasedIndexResponse);
mockClient.asCurrentUser.indices.stats.mockImplementationOnce(() => aliasedStatsResponse);
await expect(
fetchIndices(mockClient as unknown as IScopedClusterClient, 'search-*', false, true)
).resolves.toEqual([
{
count: 100,
health: 'green',
hidden: false,
name: 'index-without-prefix',
status: 'open',
alias: false,
privileges: { read: true, manage: true },
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
{
count: 100,
health: 'green',
hidden: false,
name: 'search-aliased',
status: 'open',
alias: true,
privileges: { read: true, manage: true },
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
{
count: 100,
health: 'green',
hidden: false,
name: 'search-double-aliased',
status: 'open',
alias: true,
privileges: { read: true, manage: true },
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
{
count: 100,
health: 'green',
hidden: false,
name: 'second-index',
status: 'open',
alias: false,
privileges: { read: true, manage: true },
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
]);
});
it('should return index but not aliases when aliases excluded', async () => {
const aliasedIndexResponse = {
'index-without-prefix': {
...regularIndexResponse['search-regular-index'],
aliases: {
'search-aliased': {},
'search-double-aliased': {},
},
},
'second-index': {
...regularIndexResponse['search-regular-index'],
aliases: {
'search-aliased': {},
},
},
};
const aliasedStatsResponse = {
indices: {
'index-without-prefix': { ...regularIndexStatsResponse.indices['search-regular-index'] },
'second-index': { ...regularIndexStatsResponse.indices['search-regular-index'] },
},
};
mockClient.asCurrentUser.indices.get.mockImplementationOnce(() => aliasedIndexResponse);
mockClient.asCurrentUser.indices.stats.mockImplementationOnce(() => aliasedStatsResponse);
await expect(
fetchIndices(mockClient as unknown as IScopedClusterClient, 'search-*', false, false)
).resolves.toEqual([
{
count: 100,
health: 'green',
hidden: false,
name: 'index-without-prefix',
status: 'open',
alias: false,
privileges: { read: true, manage: true },
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
{
count: 100,
health: 'green',
hidden: false,
name: 'second-index',
status: 'open',
alias: false,
privileges: { read: true, manage: true },
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
]);
});
it('should handle index missing in stats call', async () => {
const missingStatsResponse = {
indices: {
some_other_index: { ...regularIndexStatsResponse.indices['search-regular-index'] },
},
};
mockClient.asCurrentUser.indices.get.mockImplementationOnce(() => regularIndexResponse);
mockClient.asCurrentUser.indices.stats.mockImplementationOnce(() => missingStatsResponse);
// simulates when an index has been deleted after get indices call
// deleted index won't be present in the indices stats call response
await expect(
fetchIndices(mockClient as unknown as IScopedClusterClient, 'search-*', false, true)
).resolves.toEqual([
{
count: 100,
health: undefined,
hidden: false,
name: 'search-regular-index',
status: undefined,
alias: false,
privileges: { read: true, manage: true },
total: {
docs: {
count: 0,
deleted: 0,
},
store: {
size_in_bytes: '0b',
},
},
uuid: undefined,
},
]);
});
it('should return empty array when no index found', async () => {
mockClient.asCurrentUser.indices.get.mockImplementationOnce(() => ({}));
await expect(
fetchIndices(mockClient as unknown as IScopedClusterClient, 'search-*', false, true)
).resolves.toEqual([]);
expect(mockClient.asCurrentUser.indices.stats).not.toHaveBeenCalled();
});
describe('alwaysShowPattern', () => {
const sortIndices = (index1: any, index2: any) => {
if (index1.name < index2.name) return -1;
if (index1.name > index2.name) return 1;
return 0;
};
beforeEach(() => {
mockClient.asCurrentUser.indices.get.mockImplementation(() => mockMultiIndexResponse);
mockClient.asCurrentUser.indices.stats.mockImplementation(() => mockMultiStatsResponse);
mockClient.asCurrentUser.security.hasPrivileges.mockImplementation(() => ({
index: mockPrivilegesResponse,
}));
});
it('overrides hidden indices setting', async () => {
const returnValue = await fetchIndices(
mockClient as unknown as IScopedClusterClient,
'*',
false,
true,
{ alias_pattern: 'search-', index_pattern: '.ent-search-engine-documents' }
);
// This is the list of mock indices and aliases that are:
// - Non-hidden indices and aliases
// - hidden indices that starts with ".ent-search-engine-documents"
// - search- prefixed aliases that point to hidden indices
expect(returnValue.sort(sortIndices)).toEqual(
[
'regular-index',
'alias-regular-index',
'search-alias-regular-index',
'search-prefixed-regular-index',
'alias-search-prefixed-regular-index',
'search-alias-search-prefixed-regular-index',
'.ent-search-engine-documents-12345',
'search-alias-.ent-search-engine-documents-12345',
'search-alias-search-prefixed-.ent-search-engine-documents-12345',
'search-alias-hidden-index',
'search-alias-search-prefixed-hidden-index',
]
.map(getIndexReturnValue)
.sort(sortIndices)
);
// This is the list of mock indices and aliases that are:
// - Hidden indices
// - aliases to hidden indices that has no prefix
expect(returnValue).toEqual(
expect.not.arrayContaining(
// This is the list of mock indices and aliases that are:
// - Non-hidden indices and aliases
// - hidden indices that starts with ".ent-search-engine-documents"
// - search- prefixed aliases that point to hidden indices
expect(returnValue.sort(sortIndices)).toEqual(
[
'hidden-index',
'search-prefixed-hidden-index',
'alias-hidden-index',
'alias-search-prefixed-hidden-index',
'alias-.ent-search-engine-documents-12345',
'search-prefixed-.ent-search-engine-documents-12345',
'alias-search-prefixed-.ent-search-engine-documents-12345',
].map(getIndexReturnValue)
)
);
'regular-index',
'alias-regular-index',
'search-alias-regular-index',
'search-prefixed-regular-index',
'alias-search-prefixed-regular-index',
'search-alias-search-prefixed-regular-index',
'.ent-search-engine-documents-12345',
'search-alias-.ent-search-engine-documents-12345',
'search-alias-search-prefixed-.ent-search-engine-documents-12345',
'search-alias-hidden-index',
'search-alias-search-prefixed-hidden-index',
]
.map(getIndexReturnValue)
.sort(sortIndices)
);
// This is the list of mock indices and aliases that are:
// - Hidden indices
// - aliases to hidden indices that has no prefix
expect(returnValue).toEqual(
expect.not.arrayContaining(
[
'hidden-index',
'search-prefixed-hidden-index',
'alias-hidden-index',
'alias-search-prefixed-hidden-index',
'alias-.ent-search-engine-documents-12345',
'search-prefixed-.ent-search-engine-documents-12345',
'alias-search-prefixed-.ent-search-engine-documents-12345',
].map(getIndexReturnValue)
)
);
expect(mockClient.asCurrentUser.indices.get).toHaveBeenCalledWith({
expand_wildcards: ['hidden', 'all'],
features: ['aliases', 'settings'],
filter_path: ['*.aliases', '*.settings.index.hidden'],
index: '*',
});
expect(mockClient.asCurrentUser.indices.stats).toHaveBeenCalledWith({
expand_wildcards: ['hidden', 'all'],
index: '*',
metric: ['docs', 'store'],
});
expect(mockClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({
index: [
{
names: expect.arrayContaining(Object.keys(mockMultiStatsResponse.indices)),
privileges: ['read', 'manage'],
},
],
});
});
});
});
describe('fetchIndices', () => {
it('should return regular index without aliases', async () => {
mockClient.asCurrentUser.indices.get.mockImplementation(() => ({
...regularIndexResponse,
hidden: { aliases: {}, settings: { index: { hidden: 'true' } } },
}));
mockClient.asCurrentUser.indices.stats.mockImplementation(() => regularIndexStatsResponse);
await expect(
fetchIndices(mockClient as unknown as IScopedClusterClient, 'search', false, false, 0, 20)
).resolves.toEqual({
indexNames: ['search-regular-index'],
indices: [
{
alias: false,
aliases: [],
count: 100,
health: 'green',
hidden: false,
name: 'search-regular-index',
privileges: { manage: true, read: true },
status: 'open',
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
],
totalResults: 1,
});
expect(mockClient.asCurrentUser.indices.get).toHaveBeenCalledWith({
expand_wildcards: ['hidden', 'all'],
expand_wildcards: ['open'],
features: ['aliases', 'settings'],
filter_path: ['*.aliases', '*.settings.index.hidden'],
index: '*',
index: '*search*',
});
expect(mockClient.asCurrentUser.indices.stats).toHaveBeenCalledWith({
expand_wildcards: ['hidden', 'all'],
index: '*',
index: ['search-regular-index'],
metric: ['docs', 'store'],
});
expect(mockClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({
index: [
{
names: expect.arrayContaining(Object.keys(mockMultiStatsResponse.indices)),
names: ['search-regular-index'],
privileges: ['read', 'manage'],
},
],
});
});
it('returns everything if hidden indices set', async () => {
const returnValue = await fetchIndices(
mockClient as unknown as IScopedClusterClient,
'*',
true,
true,
{ alias_pattern: 'search-', index_pattern: '.ent-search-engine-documents' }
);
expect(returnValue).toEqual(
expect.not.arrayContaining(['alias-.ent-search-engine-documents-12345'])
);
// this specific alias should not be returned because...
const expectedIndices = Object.keys(mockMultiStatsResponse.indices).filter(
(indexName) => indexName !== 'alias-.ent-search-engine-documents-12345'
);
expect(returnValue.sort(sortIndices)).toEqual(
expectedIndices.map(getIndexReturnValue).sort(sortIndices)
);
it('should return hidden indices if specified', async () => {
mockClient.asCurrentUser.indices.get.mockImplementation(() => ({
...regularIndexResponse,
['search-regular-index']: {
...regularIndexResponse['search-regular-index'],
...{ settings: { index: { hidden: 'true' } } },
},
}));
mockClient.asCurrentUser.indices.stats.mockImplementation(() => regularIndexStatsResponse);
await expect(
fetchIndices(mockClient as unknown as IScopedClusterClient, undefined, true, false, 0, 20)
).resolves.toEqual({
indexNames: ['search-regular-index'],
indices: [
{
alias: false,
aliases: [],
count: 100,
health: 'green',
hidden: true,
name: 'search-regular-index',
privileges: { manage: true, read: true },
status: 'open',
total: {
docs: {
count: 100,
deleted: 0,
},
store: {
size_in_bytes: '105.47kb',
},
},
uuid: '83a81e7e-5955-4255-b008-5d6961203f57',
},
],
totalResults: 1,
});
expect(mockClient.asCurrentUser.indices.get).toHaveBeenCalledWith({
expand_wildcards: ['hidden', 'all'],
features: ['aliases', 'settings'],
@ -501,19 +368,58 @@ describe('fetchIndices lib function', () => {
});
expect(mockClient.asCurrentUser.indices.stats).toHaveBeenCalledWith({
expand_wildcards: ['hidden', 'all'],
index: '*',
index: ['search-regular-index'],
metric: ['docs', 'store'],
});
});
expect(mockClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({
index: [
it('should handle index missing in stats call', async () => {
const missingStatsResponse = {
indices: {
some_other_index: { ...regularIndexStatsResponse.indices['search-regular-index'] },
},
};
mockClient.asCurrentUser.indices.get.mockImplementationOnce(() => regularIndexResponse);
mockClient.asCurrentUser.indices.stats.mockImplementationOnce(() => missingStatsResponse);
// simulates when an index has been deleted after get indices call
// deleted index won't be present in the indices stats call response
await expect(
fetchIndices(mockClient as unknown as IScopedClusterClient, 'search-*', false, false, 0, 20)
).resolves.toEqual({
indexNames: ['search-regular-index'],
indices: [
{
names: expect.arrayContaining(Object.keys(mockMultiStatsResponse.indices)),
privileges: ['read', 'manage'],
alias: false,
aliases: [],
count: 100,
health: undefined,
hidden: false,
name: 'search-regular-index',
status: undefined,
privileges: { read: true, manage: true },
total: {
docs: {
count: 0,
deleted: 0,
},
store: {
size_in_bytes: '0b',
},
},
uuid: undefined,
},
],
totalResults: 1,
});
});
it('should return empty array when no index found', async () => {
mockClient.asCurrentUser.indices.get.mockImplementationOnce(() => ({}));
await expect(
fetchIndices(mockClient as unknown as IScopedClusterClient, 'search-*', false, false, 0, 20)
).resolves.toEqual({ indexNames: [], indices: [], totalResults: 0 });
expect(mockClient.asCurrentUser.indices.stats).not.toHaveBeenCalled();
});
});
});

View file

@ -14,13 +14,14 @@ import {
import { IScopedClusterClient } from '@kbn/core/server';
import { AlwaysShowPattern, ElasticsearchIndexWithPrivileges } from '../../../common/types/indices';
import { isNotNullish } from '../../../common/utils/is_not_nullish';
import { fetchIndexCounts } from './fetch_index_counts';
import { fetchIndexPrivileges } from './fetch_index_privileges';
import { fetchIndexStats } from './fetch_index_stats';
import { expandAliases, getAlwaysShowAliases } from './utils/extract_always_show_indices';
import { getIndexDataMapper } from './utils/get_index_data';
import { getIndexData } from './utils/get_index_data';
import { getIndexData, getSearchIndexData } from './utils/get_index_data';
export interface TotalIndexData {
allIndexMatches: IndicesGetResponse;
@ -29,35 +30,23 @@ export interface TotalIndexData {
indicesStats: Record<string, IndicesStatsIndicesStats>;
}
export const fetchIndices = async (
export const fetchSearchIndices = async (
client: IScopedClusterClient,
indexPattern: string,
returnHiddenIndices: boolean,
includeAliases: boolean,
alwaysShowPattern?: AlwaysShowPattern
): Promise<ElasticsearchIndexWithPrivileges[]> => {
// This call retrieves alias and settings information about indices
// If we provide an override pattern with alwaysShowPattern we get everything and filter out hiddens.
alwaysShowPattern: AlwaysShowPattern
) => {
const expandWildcards: ExpandWildcard[] =
returnHiddenIndices || alwaysShowPattern?.alias_pattern || alwaysShowPattern?.index_pattern
alwaysShowPattern.alias_pattern || alwaysShowPattern.index_pattern
? ['hidden', 'all']
: ['open'];
const { allIndexMatches, indexAndAliasNames, indicesNames, alwaysShowMatchNames } =
await getIndexData(
client,
indexPattern,
expandWildcards,
returnHiddenIndices,
includeAliases,
alwaysShowPattern
);
await getSearchIndexData(client, '*', expandWildcards, false, true, alwaysShowPattern);
if (indicesNames.length === 0) {
return [];
}
const indicesStats = await fetchIndexStats(client, indexPattern, expandWildcards);
const indicesStats = await fetchIndexStats(client, '*', expandWildcards);
const indexPrivileges = await fetchIndexPrivileges(client, indexAndAliasNames);
@ -81,23 +70,21 @@ export const fetchIndices = async (
name,
privileges: { manage: false, read: false, ...indexPrivileges[name] },
};
return includeAliases
? [
indexEntry,
...expandAliases(
name,
aliases,
indexData,
totalIndexData,
...(name.startsWith('.ent-search-engine-documents') ? [alwaysShowPattern] : [])
),
]
: [indexEntry];
return [
indexEntry,
...expandAliases(
name,
aliases,
indexData,
totalIndexData,
...(name.startsWith('.ent-search-engine-documents') ? [alwaysShowPattern] : [])
),
];
});
let indicesData = regularIndexData;
if (alwaysShowPattern?.alias_pattern && includeAliases) {
if (alwaysShowPattern?.alias_pattern) {
const indexNamesAlreadyIncluded = regularIndexData.map(({ name }) => name);
const itemsToInclude = getAlwaysShowAliases(indexNamesAlreadyIncluded, alwaysShowMatchNames)
@ -116,3 +103,65 @@ export const fetchIndices = async (
array.findIndex((engineData) => engineData.name === name) === index
);
};
export const fetchIndices = async (
client: IScopedClusterClient,
searchQuery: string | undefined,
returnHiddenIndices: boolean,
onlyShowSearchOptimizedIndices: boolean,
from: number,
size: number
): Promise<{
indexNames: string[];
indices: ElasticsearchIndexWithPrivileges[];
totalResults: number;
}> => {
const { indexData, indexNames } = await getIndexData(
client,
onlyShowSearchOptimizedIndices,
returnHiddenIndices,
searchQuery
);
const indexNameSlice = indexNames.slice(from, from + size).filter(isNotNullish);
if (indexNameSlice.length === 0) {
return {
indexNames: [],
indices: [],
totalResults: indexNames.length,
};
}
const { indices: indicesStats = {} } = await client.asCurrentUser.indices.stats({
index: indexNameSlice,
metric: ['docs', 'store'],
});
const indexPrivileges = await fetchIndexPrivileges(client, indexNameSlice);
const indexCounts = await fetchIndexCounts(client, indexNameSlice);
const totalIndexData: TotalIndexData = {
allIndexMatches: indexData,
indexCounts,
indexPrivileges,
indicesStats,
};
const indices = indexNameSlice
.map(getIndexDataMapper(totalIndexData))
.map(({ name, ...index }) => {
return {
...index,
alias: false,
count: indexCounts[name] ?? 0,
name,
privileges: { manage: false, read: false, ...indexPrivileges[name] },
};
});
return {
indexNames: indexNameSlice,
indices,
totalResults: indexNames.length,
};
};

View file

@ -14,7 +14,7 @@ import { IScopedClusterClient } from '@kbn/core/server';
import { TotalIndexData } from '../fetch_indices';
import { getIndexData, getIndexDataMapper } from './get_index_data';
import { getIndexDataMapper, getSearchIndexData } from './get_index_data';
import * as mapIndexStatsModule from './map_index_stats';
@ -34,7 +34,7 @@ describe('getIndexData util function', () => {
return mockSingleIndexWithAliasesResponse;
});
const indexData = await getIndexData(
const indexData = await getSearchIndexData(
mockClient as unknown as IScopedClusterClient,
'*',
['open'],
@ -63,7 +63,7 @@ describe('getIndexData util function', () => {
return mockSingleIndexWithAliasesResponse;
});
const indexData = await getIndexData(
const indexData = await getSearchIndexData(
mockClient as unknown as IScopedClusterClient,
'*',
['open'],
@ -92,7 +92,7 @@ describe('getIndexData util function', () => {
return mockMultiIndexResponse;
});
const indexData = await getIndexData(
const indexData = await getSearchIndexData(
mockClient as unknown as IScopedClusterClient,
'*',
['hidden', 'all'],
@ -135,7 +135,7 @@ describe('getIndexData util function', () => {
return mockMultiIndexResponse;
});
const indexData = await getIndexData(
const indexData = await getSearchIndexData(
mockClient as unknown as IScopedClusterClient,
'*',
['hidden', 'all'],
@ -195,7 +195,7 @@ describe('getIndexData util function', () => {
return mockMultiIndexResponse;
});
const indexData = await getIndexData(
const indexData = await getSearchIndexData(
mockClient as unknown as IScopedClusterClient,
'*',
['hidden', 'all'],

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import { ExpandWildcard } from '@elastic/elasticsearch/lib/api/types';
import {
ExpandWildcard,
IndicesGetResponse,
IndicesIndexState,
} from '@elastic/elasticsearch/lib/api/types';
import { IScopedClusterClient } from '@kbn/core/server';
@ -15,7 +19,7 @@ import { TotalIndexData } from '../fetch_indices';
import { mapIndexStats } from './map_index_stats';
export const getIndexData = async (
export const getSearchIndexData = async (
client: IScopedClusterClient,
indexPattern: string,
expandWildcards: ExpandWildcard[],
@ -63,7 +67,7 @@ export const getIndexData = async (
? Object.keys(totalIndices)
: Object.keys(totalIndices).filter(
(indexName) =>
!(totalIndices[indexName]?.settings?.index?.hidden === 'true') ||
!isHidden(totalIndices[indexName]) ||
(alwaysShowPattern?.index_pattern &&
indexName.startsWith(alwaysShowPattern.index_pattern))
);
@ -84,3 +88,41 @@ export const getIndexDataMapper = (totalIndexData: TotalIndexData) => {
indexName
);
};
function isHidden(index: IndicesIndexState): boolean {
return Boolean(index.settings?.index?.hidden) || index.settings?.index?.hidden === 'true';
}
export const getIndexData = async (
client: IScopedClusterClient,
onlyShowSearchOptimizedIndices: boolean,
returnHiddenIndices: boolean,
searchQuery?: string
): Promise<{ indexData: IndicesGetResponse; indexNames: string[] }> => {
const expandWildcards: ExpandWildcard[] = returnHiddenIndices ? ['hidden', 'all'] : ['open'];
const indexPattern = searchQuery ? `*${searchQuery}*` : '*';
const allIndexMatches = await client.asCurrentUser.indices.get({
expand_wildcards: expandWildcards,
// for better performance only compute aliases and settings of indices but not mappings
features: ['aliases', 'settings'],
// only get specified index properties from ES to keep the response under 536MB
// node.js string length limit: https://github.com/nodejs/node/issues/33960
filter_path: ['*.aliases', '*.settings.index.hidden'],
index: onlyShowSearchOptimizedIndices ? 'search-*' : indexPattern,
});
const allIndexNames = returnHiddenIndices
? Object.keys(allIndexMatches)
: Object.keys(allIndexMatches).filter(
(indexName) => allIndexMatches[indexName] && !isHidden(allIndexMatches[indexName])
);
const indexNames =
onlyShowSearchOptimizedIndices && searchQuery
? allIndexNames.filter((indexName) => indexName.includes(searchQuery.toLowerCase()))
: allIndexNames;
return {
indexData: allIndexMatches,
indexNames,
};
};

View file

@ -28,7 +28,7 @@ import { fetchCrawlerByIndexName, fetchCrawlers } from '../../lib/crawler/fetch_
import { createIndex } from '../../lib/indices/create_index';
import { indexOrAliasExists } from '../../lib/indices/exists_index';
import { fetchIndex } from '../../lib/indices/fetch_index';
import { fetchIndices } from '../../lib/indices/fetch_indices';
import { fetchIndices, fetchSearchIndices } from '../../lib/indices/fetch_indices';
import { generateApiKey } from '../../lib/indices/generate_api_key';
import { getMlInferenceErrors } from '../../lib/indices/pipelines/ml_inference/get_ml_inference_errors';
import { fetchMlInferencePipelineHistory } from '../../lib/indices/pipelines/ml_inference/get_ml_inference_pipeline_history';
@ -67,7 +67,7 @@ export function registerIndexRoutes({
alias_pattern: 'search-',
index_pattern: '.ent-search-engine-documents',
};
const indices = await fetchIndices(client, '*', false, true, patterns);
const indices = await fetchSearchIndices(client, patterns);
return response.ok({
body: indices,
@ -81,7 +81,8 @@ export function registerIndexRoutes({
path: '/internal/enterprise_search/indices',
validate: {
query: schema.object({
page: schema.number({ defaultValue: 0, min: 0 }),
from: schema.number({ defaultValue: 0, min: 0 }),
only_show_search_optimized_indices: schema.maybe(schema.boolean()),
return_hidden_indices: schema.maybe(schema.boolean()),
search_query: schema.maybe(schema.string()),
size: schema.number({ defaultValue: 10, min: 0 }),
@ -90,24 +91,25 @@ export function registerIndexRoutes({
},
elasticsearchErrorHandler(log, async (context, request, response) => {
const {
page,
from,
only_show_search_optimized_indices: onlyShowSearchOptimizedIndices,
size,
return_hidden_indices: returnHiddenIndices,
search_query: searchQuery,
} = request.query;
const { client } = (await context.core).elasticsearch;
const indexPattern = searchQuery ? `*${searchQuery}*` : '*';
const totalIndices = await fetchIndices(client, indexPattern, !!returnHiddenIndices, false);
const totalResults = totalIndices.length;
const totalPages = Math.ceil(totalResults / size) || 1;
const startIndex = (page - 1) * size;
const endIndex = page * size;
const selectedIndices = totalIndices.slice(startIndex, endIndex);
const indexNames = selectedIndices.map(({ name }) => name);
const { indexNames, indices, totalResults } = await fetchIndices(
client,
searchQuery,
!!returnHiddenIndices,
!!onlyShowSearchOptimizedIndices,
from,
size
);
const connectors = await fetchConnectors(client, indexNames);
const crawlers = await fetchCrawlers(client, indexNames);
const indices = selectedIndices.map((index) => ({
const enrichedIndices = indices.map((index) => ({
...index,
connector: connectors.find((connector) => connector.index_name === index.name),
crawler: crawlers.find((crawler) => crawler.index_name === index.name),
@ -115,13 +117,12 @@ export function registerIndexRoutes({
return response.ok({
body: {
indices,
indices: enrichedIndices,
meta: {
page: {
current: page,
from,
size,
total_pages: totalPages,
total_results: totalResults,
total: totalResults,
},
},
},