[Enterprise Search] [Search application] List indices fetch indices stats from ES directly (#153696)

## Summary

This PR adds a lib file to fetch indices stats of an search application from Elasticsearch directly

### Screenshot

#### List page indices 
<img width="1728" alt="List page indices"
src="https://user-images.githubusercontent.com/55930906/227617672-bfcdd3e4-85c2-4432-bcee-2aa3f5be07a4.png">

#### Overview page indices 
<img width="1728" alt="Overview page indices "
src="https://user-images.githubusercontent.com/55930906/227617828-afa01bf6-5f23-4f3f-9ff6-75f6c5dbcff2.png">

### Checklist


- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Saarika Bhasi 2023-03-30 11:56:47 -04:00 committed by GitHub
parent b002cdc6eb
commit 327dd493bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 181 additions and 34 deletions

View file

@ -16,11 +16,11 @@ import { FetchEngineApiLogic } from '../../api/engines/fetch_engine_api_logic';
import { EngineListFlyoutValues, EnginesListFlyoutLogic } from './engines_list_flyout_logic';
const DEFAULT_VALUES: EngineListFlyoutValues = {
fetchEngineApiError: undefined,
fetchEngineApiStatus: Status.IDLE,
fetchEngineData: undefined,
fetchEngineName: null,
isFetchEngineFlyoutVisible: false,
fetchEngineApiStatus: Status.IDLE,
fetchEngineApiError: undefined,
isFetchEngineLoading: false,
};
const mockEngineData: EnterpriseSearchEngineDetails = {
@ -66,10 +66,10 @@ describe('EngineListFlyoutLogic', () => {
EnginesListFlyoutLogic.actions.openFetchEngineFlyout('my-test-engine');
expect(EnginesListFlyoutLogic.values).toEqual({
...DEFAULT_VALUES,
isFetchEngineFlyoutVisible: true,
fetchEngineName: 'my-test-engine',
isFetchEngineLoading: true,
fetchEngineApiStatus: Status.LOADING,
fetchEngineName: 'my-test-engine',
isFetchEngineFlyoutVisible: true,
isFetchEngineLoading: true,
});
});
});

View file

@ -50,8 +50,8 @@ export type EnginesListActions = Pick<
openDeleteEngineModal: (engine: EnterpriseSearchEngine | EnterpriseSearchEngineDetails) => {
engine: EnterpriseSearchEngine;
};
setIsFirstRequest(): void;
openEngineCreate(): void;
setIsFirstRequest(): void;
setSearchQuery(searchQuery: string): { searchQuery: string };
};
@ -74,6 +74,16 @@ interface EngineListValues {
}
export const EnginesListLogic = kea<MakeLogicType<EngineListValues, EnginesListActions>>({
actions: {
closeDeleteEngineModal: true,
closeEngineCreate: true,
fetchEngines: true,
onPaginate: (args: EuiBasicTableOnChange) => ({ pageNumber: args.page.index }),
openDeleteEngineModal: (engine) => ({ engine }),
openEngineCreate: true,
setIsFirstRequest: true,
setSearchQuery: (searchQuery: string) => ({ searchQuery }),
},
connect: {
actions: [
FetchEnginesAPILogic,
@ -88,17 +98,18 @@ export const EnginesListLogic = kea<MakeLogicType<EngineListValues, EnginesListA
['status as deleteStatus'],
],
},
actions: {
closeDeleteEngineModal: true,
closeEngineCreate: true,
fetchEngines: true,
onPaginate: (args: EuiBasicTableOnChange) => ({ pageNumber: args.page.index }),
openDeleteEngineModal: (engine) => ({ engine }),
openEngineCreate: true,
setSearchQuery: (searchQuery: string) => ({ searchQuery }),
setIsFirstRequest: true,
},
listeners: ({ actions, values }) => ({
deleteSuccess: () => {
actions.closeDeleteEngineModal();
actions.fetchEngines();
},
fetchEngines: async () => {
actions.makeRequest(values.parameters);
},
}),
path: ['enterprise_search', 'content', 'engine_list_logic'],
reducers: ({}) => ({
createEngineFlyoutOpen: [
false,
@ -158,6 +169,11 @@ export const EnginesListLogic = kea<MakeLogicType<EngineListValues, EnginesListA
}),
selectors: ({ selectors }) => ({
deleteModalEngineName: [() => [selectors.deleteModalEngine], (engine) => engine?.name ?? ''],
hasNoEngines: [
() => [selectors.data, selectors.results],
(data: EngineListValues['data'], results: EngineListValues['results']) =>
(data?.params?.from === 0 && results.length === 0 && !data?.params?.q) ?? false,
],
isDeleteLoading: [
() => [selectors.deleteStatus],
@ -168,22 +184,7 @@ export const EnginesListLogic = kea<MakeLogicType<EngineListValues, EnginesListA
(status: EngineListValues['status'], isFirstRequest: EngineListValues['isFirstRequest']) =>
[Status.LOADING, Status.IDLE].includes(status) && isFirstRequest,
],
results: [() => [selectors.data], (data) => data?.results ?? []],
hasNoEngines: [
() => [selectors.data, selectors.results],
(data: EngineListValues['data'], results: EngineListValues['results']) =>
(data?.params?.from === 0 && results.length === 0 && !data?.params?.q) ?? false,
],
meta: [() => [selectors.parameters], (parameters) => parameters.meta],
}),
listeners: ({ actions, values }) => ({
deleteSuccess: () => {
actions.closeDeleteEngineModal();
actions.fetchEngines();
},
fetchEngines: async () => {
actions.makeRequest(values.parameters);
},
results: [() => [selectors.data], (data) => data?.results ?? []],
}),
});

View file

@ -0,0 +1,95 @@
/*
* 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 { fetchIndicesStats } from './fetch_indices_stats';
describe('fetchIndicesStats lib function', () => {
const mockClient = {
asCurrentUser: {
indices: {
stats: jest.fn(),
},
},
asInternalUser: {},
};
const indices = ['test-index-name-1', 'test-index-name-2', 'test-index-name-3'];
const indicesStats = {
indices: {
'test-index-name-1': {
health: 'GREEN',
primaries: { docs: [{}] },
status: 'open',
total: {
docs: {
count: 200,
deleted: 0,
},
},
uuid: 'YOLLiZ_mSRiDYDk0DJ-p8B',
},
'test-index-name-2': {
health: 'YELLOW',
primaries: { docs: [{}] },
status: 'closed',
total: {
docs: {
count: 0,
deleted: 0,
},
},
uuid: 'QOLLiZ_mGRiDYD30D2-p8B',
},
'test-index-name-3': {
health: 'RED',
primaries: { docs: [{}] },
status: 'open',
total: {
docs: {
count: 150,
deleted: 0,
},
},
uuid: 'QYLLiZ_fGRiDYD3082-e7',
},
},
};
const fetchIndicesStatsResponse = [
{
count: 200,
health: 'GREEN',
name: 'test-index-name-1',
},
{
count: 0,
health: 'YELLOW',
name: 'test-index-name-2',
},
{
count: 150,
health: 'RED',
name: 'test-index-name-3',
},
];
beforeEach(() => {
jest.clearAllMocks();
});
it('should return hydrated indices', async () => {
mockClient.asCurrentUser.indices.stats.mockImplementationOnce(() => indicesStats);
await expect(
fetchIndicesStats(mockClient as unknown as IScopedClusterClient, indices)
).resolves.toEqual(fetchIndicesStatsResponse);
expect(mockClient.asCurrentUser.indices.stats).toHaveBeenCalledWith({
index: indices,
metric: ['docs'],
});
});
});

View file

@ -0,0 +1,29 @@
/*
* 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-elasticsearch-server/src/client/scoped_cluster_client';
import { EnterpriseSearchEngineIndex } from '../../../common/types/engines';
export const fetchIndicesStats = async (client: IScopedClusterClient, indices: string[]) => {
const { indices: indicesStats = {} } = await client.asCurrentUser.indices.stats({
index: indices,
metric: ['docs'],
});
const indicesWithStats = indices.map((indexName: string) => {
const indexStats = indicesStats[indexName];
const hydratedIndex: EnterpriseSearchEngineIndex = {
count: indexStats?.total?.docs?.count ?? 0,
health: indexStats?.health ?? 'unknown',
name: indexName,
};
return hydratedIndex;
});
return indicesWithStats;
};

View file

@ -10,9 +10,12 @@ import { mockDependencies, MockRouter } from '../../__mocks__';
jest.mock('../../lib/engines/field_capabilities', () => ({
fetchEngineFieldCapabilities: jest.fn(),
}));
jest.mock('../../lib/engines/fetch_indices_stats', () => ({
fetchIndicesStats: jest.fn(),
}));
import { RequestHandlerContext } from '@kbn/core/server';
import { fetchIndicesStats } from '../../lib/engines/fetch_indices_stats';
import { fetchEngineFieldCapabilities } from '../../lib/engines/field_capabilities';
import { registerEnginesRoutes } from './engines';
@ -115,6 +118,22 @@ describe('engines routes', () => {
method: 'GET',
path: '/_application/search_application/engine-name',
});
const mock = jest.fn();
const fetchIndicesStatsResponse = [
{ count: 5, health: 'green', name: 'test-index-name-1' },
{ count: 10, health: 'yellow', name: 'test-index-name-2' },
{ count: 0, health: 'red', name: 'test-index-name-3' },
];
const engineResult = {
indices: mock(['test-index-name-1', 'test-index-name-2', 'test-index-name-3']),
name: 'test-engine-1',
updated_at_millis: 1679847286355,
};
(fetchIndicesStats as jest.Mock).mockResolvedValueOnce(fetchIndicesStatsResponse);
expect(fetchIndicesStats).toHaveBeenCalledWith(mockClient, engineResult.indices);
expect(mockRouter.response.ok).toHaveBeenCalledWith({
body: {},
});

View file

@ -14,6 +14,7 @@ import {
} from '../../../common/types/engines';
import { ErrorCode } from '../../../common/types/error_codes';
import { createApiKey } from '../../lib/engines/create_api_key';
import { fetchIndicesStats } from '../../lib/engines/fetch_indices_stats';
import { fetchEngineFieldCapabilities } from '../../lib/engines/field_capabilities';
import { RouteDependencies } from '../../plugin';
@ -63,7 +64,9 @@ export function registerEnginesRoutes({ log, router }: RouteDependencies) {
method: 'GET',
path: `/_application/search_application/${request.params.engine_name}`,
});
return response.ok({ body: engine });
const indicesStats = await fetchIndicesStats(client, engine.indices);
return response.ok({ body: { ...engine, indices: indicesStats } });
})
);