mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
30a05bdb7c
commit
de32cac471
7 changed files with 380 additions and 4 deletions
|
@ -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>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
|
|
|
@ -127,6 +127,7 @@ describe('EnginesListLogic', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reducers', () => {
|
||||
describe('meta', () => {
|
||||
it('updates when apiSuccess', () => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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] ?? ''} />
|
||||
{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 <></>;
|
||||
}
|
||||
};
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
}),
|
||||
});
|
|
@ -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),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue