[Enterprise Search] Engine list - Show indices flyout (#149241)

## Summary

Engine list - show indices flyout


https://user-images.githubusercontent.com/55930906/213495039-48713d0c-1c47-4af9-bc8c-41f71a9b8781.mov


When the Api Status returns Error

<img width="1715" alt="error"
src="https://user-images.githubusercontent.com/55930906/214635794-36ee03ed-4440-4730-a9c7-17eda3084706.png">

### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [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: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Saarika Bhasi 2023-01-25 16:25:16 -05:00 committed by GitHub
parent 30a05bdb7c
commit de32cac471
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 380 additions and 4 deletions

View file

@ -9,9 +9,15 @@ import React from 'react';
import { useValues } from 'kea';
import { CriteriaWithPagination, EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui';
import {
CriteriaWithPagination,
EuiBasicTable,
EuiBasicTableColumn,
EuiButtonEmpty,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EnterpriseSearchEngine } from '../../../../../../../common/types/engines';
import { MANAGE_BUTTON_LABEL } from '../../../../../shared/constants';
@ -25,7 +31,6 @@ import { ENGINE_PATH } from '../../../../routes';
import { convertMetaToPagination, Meta } from '../../types';
// add health status
interface EnginesListTableProps {
enginesList: EnterpriseSearchEngine[];
isLoading?: boolean;
@ -33,6 +38,7 @@ interface EnginesListTableProps {
meta: Meta;
onChange: (criteria: CriteriaWithPagination<EnterpriseSearchEngine>) => void;
onDelete: (engine: EnterpriseSearchEngine) => void;
viewEngineIndices: (engineName: string) => void;
}
export const EnginesListTable: React.FC<EnginesListTableProps> = ({
enginesList,
@ -40,6 +46,7 @@ export const EnginesListTable: React.FC<EnginesListTableProps> = ({
meta,
onChange,
onDelete,
viewEngineIndices,
}) => {
const { navigateToUrl } = useValues(KibanaLogic);
const columns: Array<EuiBasicTableColumn<EnterpriseSearchEngine>> = [
@ -74,11 +81,28 @@ export const EnginesListTable: React.FC<EnginesListTableProps> = ({
render: (dateString: string) => <FormattedDateTime date={new Date(dateString)} hideTime />,
},
{
field: 'indices.length',
datatype: 'number',
field: 'indices',
name: i18n.translate('xpack.enterpriseSearch.content.enginesList.table.column.indices', {
defaultMessage: 'Indices',
}),
align: 'right',
render: (indices: string[], engine) => (
<EuiButtonEmpty
size="s"
className="engineListTableFlyoutButton"
data-test-subj="engineListTableIndicesFlyoutButton"
onClick={() => viewEngineIndices(engine.name)}
>
<FormattedMessage
id="xpack.enterpriseSearch.content.enginesList.table.column.view.indices"
defaultMessage="{indicesLength} indices"
values={{
indicesLength: indices.length,
}}
/>
</EuiButtonEmpty>
),
},
{

View file

@ -127,6 +127,7 @@ describe('EnginesListLogic', () => {
});
});
});
describe('reducers', () => {
describe('meta', () => {
it('updates when apiSuccess', () => {

View file

@ -16,6 +16,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react';
import { INPUT_THROTTLE_DELAY_MS } from '../../../shared/constants/timers';
import { DataPanel } from '../../../shared/data_panel/data_panel';
import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_template';
@ -24,6 +25,8 @@ import { EmptyEnginesPrompt } from './components/empty_engines_prompt';
import { EnginesListTable } from './components/tables/engines_table';
import { CreateEngineFlyout } from './create_engine_flyout';
import { DeleteEngineModal } from './delete_engine_modal';
import { EngineListIndicesFlyout } from './engines_list_flyout';
import { EnginesListFlyoutLogic } from './engines_list_flyout_logic';
import { EnginesListLogic } from './engines_list_logic';
const CreateButton: React.FC = () => {
@ -46,6 +49,7 @@ const CreateButton: React.FC = () => {
export const EnginesList: React.FC = () => {
const { closeEngineCreate, fetchEngines, onPaginate, openDeleteEngineModal, setSearchQuery } =
useActions(EnginesListLogic);
const { openFetchEngineFlyout } = useActions(EnginesListFlyoutLogic);
const { isLoading, meta, results, createEngineFlyoutOpen, searchQuery } =
useValues(EnginesListLogic);
@ -58,6 +62,8 @@ export const EnginesList: React.FC = () => {
return (
<>
<DeleteEngineModal />
<EngineListIndicesFlyout />
{createEngineFlyoutOpen && <CreateEngineFlyout onClose={closeEngineCreate} />}
<EnterpriseSearchEnginesPageTemplate
pageChrome={[
@ -164,6 +170,7 @@ export const EnginesList: React.FC = () => {
meta={meta}
onChange={onPaginate}
onDelete={openDeleteEngineModal}
viewEngineIndices={openFetchEngineFlyout}
loading={false}
/>
</DataPanel>

View file

@ -0,0 +1,161 @@
/*
* 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 React from 'react';
import { useValues, useActions } from 'kea';
import {
EuiBasicTable,
EuiBasicTableColumn,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiIcon,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { Status } from '../../../../../common/types/api';
import { EnterpriseSearchEngineIndex } from '../../../../../common/types/engines';
import { healthColorsMap } from '../../../shared/constants/health_colors';
import { generateEncodedPath } from '../../../shared/encode_path_params';
import { EuiLinkTo } from '../../../shared/react_router_helpers';
import { SEARCH_INDEX_PATH } from '../../routes';
import { IngestionMethod } from '../../types';
import { ingestionMethodToText } from '../../utils/indices';
import { EngineError } from '../engine/engine_error';
import { EnginesListFlyoutLogic } from './engines_list_flyout_logic';
export const EngineListIndicesFlyout: React.FC = () => {
const {
fetchEngineData,
fetchEngineName,
isFetchEngineLoading,
isFetchEngineFlyoutVisible,
fetchEngineApiStatus,
fetchEngineApiError,
} = useValues(EnginesListFlyoutLogic);
const { closeFetchIndicesFlyout } = useActions(EnginesListFlyoutLogic);
if (!fetchEngineData) return null;
const { indices } = fetchEngineData;
const engineFetchError = fetchEngineApiStatus === Status.ERROR ? true : false;
const columns: Array<EuiBasicTableColumn<EnterpriseSearchEngineIndex>> = [
{
field: 'name',
name: i18n.translate(
'xpack.enterpriseSearch.content.enginesList.indicesFlyout.table.name.columnTitle',
{
defaultMessage: 'Index name',
}
),
render: (indexName: string) => (
<EuiLinkTo
data-test-subj="engine-index-link"
data-telemetry-id="entSearchContent-engines-list-viewIndex"
to={generateEncodedPath(SEARCH_INDEX_PATH, { indexName })}
>
{indexName}
</EuiLinkTo>
),
sortable: true,
truncateText: true,
width: '40%',
},
{
field: 'health',
name: i18n.translate(
'xpack.enterpriseSearch.content.enginesList.indicesFlyout.table.health.columnTitle',
{
defaultMessage: 'Index health',
}
),
render: (health: 'red' | 'green' | 'yellow' | 'unavailable') => (
<span>
<EuiIcon type="dot" color={healthColorsMap[health] ?? ''} />
&nbsp;{health ?? '-'}
</span>
),
sortable: true,
truncateText: true,
width: '15%',
},
{
field: 'count',
name: i18n.translate(
'xpack.enterpriseSearch.content.enginesList.indicesFlyout.table.docsCount.columnTitle',
{
defaultMessage: 'Docs count',
}
),
sortable: true,
truncateText: true,
width: '15%',
},
{
field: 'source',
name: i18n.translate(
'xpack.enterpriseSearch.content.enginesList.indicesFlyout.table.ingestionMethod.columnTitle',
{
defaultMessage: 'Ingestion method',
}
),
render: (source: IngestionMethod) => (
<EuiText size="s">{ingestionMethodToText(source)}</EuiText>
),
truncateText: true,
width: '15%',
},
];
if (isFetchEngineFlyoutVisible) {
return (
<EuiFlyout ownFocus aria-labelledby="enginesListFlyout" onClose={closeFetchIndicesFlyout}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2 id="engineListFlyout">
{i18n.translate('xpack.enterpriseSearch.content.enginesList.indicesFlyout.title', {
defaultMessage: 'View Indices',
})}
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText color="subdued">
<FormattedMessage
id="xpack.enterpriseSearch.content.enginesList.indicesFlyout.subTitle"
defaultMessage="View the indices associated with {engineName}"
values={{
engineName: fetchEngineName,
}}
/>
</EuiText>
</EuiFlyoutHeader>
<EuiFlyoutBody>
{engineFetchError ? (
<EngineError error={fetchEngineApiError} />
) : (
<EuiBasicTable items={indices} columns={columns} loading={isFetchEngineLoading} />
)}
</EuiFlyoutBody>
</EuiFlyout>
);
} else {
return <></>;
}
};

View file

@ -0,0 +1,107 @@
/*
* 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 { LogicMounter } from '../../../__mocks__/kea_logic';
import { nextTick } from '@kbn/test-jest-helpers';
import { Status } from '../../../../../common/types/api';
import { EnterpriseSearchEngineDetails } from '../../../../../common/types/engines';
import { FetchEngineApiLogic } from '../../api/engines/fetch_engine_api_logic';
import { EngineListFlyoutValues, EnginesListFlyoutLogic } from './engines_list_flyout_logic';
const DEFAULT_VALUES: EngineListFlyoutValues = {
fetchEngineData: undefined,
fetchEngineName: null,
isFetchEngineFlyoutVisible: false,
fetchEngineApiStatus: Status.IDLE,
fetchEngineApiError: undefined,
isFetchEngineLoading: false,
};
const mockEngineData: EnterpriseSearchEngineDetails = {
created: '1999-12-31T23:59:59Z',
indices: [
{
count: 10,
health: 'green',
name: 'search-001',
source: 'api',
},
{
count: 1000,
health: 'yellow',
name: 'search-002',
source: 'crawler',
},
],
name: 'my-test-engine',
updated: '1999-12-31T23:59:59Z',
};
describe('EngineListFlyoutLogic', () => {
const { mount } = new LogicMounter(EnginesListFlyoutLogic);
const { mount: apiLogicMount } = new LogicMounter(FetchEngineApiLogic);
beforeEach(() => {
jest.clearAllMocks();
jest.useRealTimers();
apiLogicMount();
mount();
});
it('has expected default values', () => {
expect(EnginesListFlyoutLogic.values).toEqual(DEFAULT_VALUES);
});
describe('actions', () => {
describe('closeFetchEngineIndicesFlyout', () => {
it('set isFetchEngineFlyoutVisible to false and fetchEngineName to empty string', () => {
EnginesListFlyoutLogic.actions.closeFetchIndicesFlyout();
expect(EnginesListFlyoutLogic.values).toEqual(DEFAULT_VALUES);
});
});
describe('openFetchEngineIndicesFlyout', () => {
it('set isFetchEngineFlyoutVisible to true and sets fetchEngineName to engine name', () => {
EnginesListFlyoutLogic.actions.openFetchEngineFlyout('my-test-engine');
expect(EnginesListFlyoutLogic.values).toEqual({
...DEFAULT_VALUES,
isFetchEngineFlyoutVisible: true,
fetchEngineName: 'my-test-engine',
isFetchEngineLoading: true,
fetchEngineApiStatus: Status.LOADING,
});
});
});
});
describe('selectors', () => {
it('receives fetchEngine indices data on success', () => {
expect(EnginesListFlyoutLogic.values).toEqual(DEFAULT_VALUES);
FetchEngineApiLogic.actions.apiSuccess(mockEngineData);
expect(EnginesListFlyoutLogic.values).toEqual({
...DEFAULT_VALUES,
fetchEngineApiStatus: Status.SUCCESS,
fetchEngineData: mockEngineData,
});
});
});
describe('listeners', () => {
beforeEach(() => {
FetchEngineApiLogic.actions.apiSuccess(mockEngineData);
});
it('fetch engines flyout when flyout is visible', async () => {
jest.useFakeTimers({ legacyFakeTimers: true });
EnginesListFlyoutLogic.actions.openFetchEngineFlyout = jest.fn();
EnginesListFlyoutLogic.actions.openFetchEngineFlyout('my-test-engine');
await nextTick();
expect(EnginesListFlyoutLogic.actions.openFetchEngineFlyout).toHaveBeenCalledTimes(1);
expect(EnginesListFlyoutLogic.actions.openFetchEngineFlyout).toHaveBeenCalledWith(
'my-test-engine'
);
});
});
});

View file

@ -0,0 +1,73 @@
/*
* 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 { kea, MakeLogicType } from 'kea';
import { Status } from '../../../../../common/types/api';
import { FetchEngineApiLogic } from '../../api/engines/fetch_engine_api_logic';
import { EngineViewActions, EngineViewLogic, EngineViewValues } from '../engine/engine_view_logic';
export interface EngineListFlyoutValues {
isFetchEngineLoading: EngineViewValues['isLoadingEngine'];
isFetchEngineFlyoutVisible: boolean;
fetchEngineData: EngineViewValues['engineData']; // data from fetchEngineAPI
fetchEngineName: string | null;
fetchEngineApiError?: EngineViewValues['fetchEngineApiError'];
fetchEngineApiStatus: EngineViewValues['fetchEngineApiStatus'];
}
export interface EngineListFlyoutActions {
closeFetchIndicesFlyout(): void;
fetchEngineData: EngineViewActions['fetchEngine'] | null;
openFetchEngineFlyout: (engineName: string) => { engineName: string };
}
export const EnginesListFlyoutLogic = kea<
MakeLogicType<EngineListFlyoutValues, EngineListFlyoutActions>
>({
connect: {
actions: [EngineViewLogic, ['fetchEngine as fetchEngine']],
values: [
EngineViewLogic,
[
'engineData as fetchEngineData',
'fetchEngineApiError as fetchEngineApiError',
'fetchEngineApiStatus as fetchEngineApiStatus',
],
],
},
actions: {
closeFetchIndicesFlyout: true,
openFetchEngineFlyout: (engineName) => ({ engineName }),
},
path: ['enterprise_search', 'content', 'engine_list_flyout_logic'],
reducers: ({}) => ({
fetchEngineName: [
null,
{
closeFetchIndicesFlyout: () => null,
openFetchEngineFlyout: (_, { engineName }) => engineName,
},
],
isFetchEngineFlyoutVisible: [
false,
{
closeFetchIndicesFlyout: () => false,
openFetchEngineFlyout: () => true,
},
],
}),
selectors: ({ selectors }) => ({
isFetchEngineLoading: [
() => [selectors.fetchEngineApiStatus],
(status: EngineListFlyoutValues['fetchEngineApiStatus']) => [Status.LOADING].includes(status),
],
}),
listeners: ({}) => ({
openFetchEngineFlyout: async (input) => {
FetchEngineApiLogic.actions.makeRequest(input);
},
}),
});

View file

@ -105,6 +105,7 @@ export const EnginesListLogic = kea<MakeLogicType<EngineListValues, EnginesListA
openDeleteEngineModal: (_, { engine }) => engine,
},
],
isDeleteModalVisible: [
false,
{
@ -112,6 +113,7 @@ export const EnginesListLogic = kea<MakeLogicType<EngineListValues, EnginesListA
openDeleteEngineModal: () => true,
},
],
parameters: [
{ meta: DEFAULT_META },
{
@ -137,6 +139,7 @@ export const EnginesListLogic = kea<MakeLogicType<EngineListValues, EnginesListA
}),
selectors: ({ selectors }) => ({
deleteModalEngineName: [() => [selectors.deleteModalEngine], (engine) => engine?.name ?? ''],
isDeleteLoading: [
() => [selectors.deleteStatus],
(status: EngineListValues['deleteStatus']) => [Status.LOADING].includes(status),