Skip to content

Commit

Permalink
[Enterprise Search] [Search application] List indices fetch indices s…
Browse files Browse the repository at this point in the history
…tats 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 <[email protected]>
  • Loading branch information
saarikabhasi and kibanamachine authored Mar 30, 2023
1 parent b002cdc commit 327dd49
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -66,10 +66,10 @@ describe('EngineListFlyoutLogic', () => {
EnginesListFlyoutLogic.actions.openFetchEngineFlyout('my-test-engine');
expect(EnginesListFlyoutLogic.values).toEqual({
...DEFAULT_VALUES,
isFetchEngineFlyoutVisible: true,
fetchEngineApiStatus: Status.LOADING,
fetchEngineName: 'my-test-engine',
isFetchEngineFlyoutVisible: true,
isFetchEngineLoading: true,
fetchEngineApiStatus: Status.LOADING,
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
};

Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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],
Expand All @@ -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 ?? []],
}),
});
Original file line number Diff line number Diff line change
@@ -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'],
});
});
});
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: {},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 } });
})
);

Expand Down

0 comments on commit 327dd49

Please sign in to comment.