[Enterprise Search] Search indices list (#135135)

* Use real API call to fetch indices
* Add Paths and pagination
* Update table layout and i18n
* Type changes and fixes
* Update api logic with new utility function

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Efe Gürkan YALAMAN 2022-06-30 15:09:58 +02:00 committed by GitHub
parent a852919b0d
commit 4908a3b5ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 488 additions and 337 deletions

View file

@ -26,7 +26,7 @@ Slicing up components into smaller chunks, designing clear interfaces for those
State management tools are most powerful when used to coordinate state across an entire application, or large slices of that application. To do that well, state needs to be shared and it needs to be clear where in the existing state to find what information. We do this by separating API data from component data.
This means API interactions and their data should live in their own logic files, and the resulting data and API status should be imported by other logic files or directly by components consuming that data. Those API logic files should contain all interactions with APIs, and the current status of those API requests. We have a util function to help you create those, located in [create_api_logic.ts](public/applications/shared/api_logic/create_api_logic.ts). You can grab the `status`, `data` and `error` values from any API created with that util. And you can listen to the `initiateCall`, `apiSuccess`, `apiError` and `apiReset` actions in other listeners.
This means API interactions and their data should live in their own logic files, and the resulting data and API status should be imported by other logic files or directly by components consuming that data. Those API logic files should contain all interactions with APIs, and the current status of those API requests. We have a util function to help you create those, located in [create_api_logic.ts](public/applications/shared/api_logic/create_api_logic.ts). You can grab the `status`, `data` and `error` values from any API created with that util. And you can listen to the `makeRequest`, `apiSuccess`, `apiError` and `apiReset` actions in other listeners.
You will need to provide a function that actually makes the api call, as well as the logic path. The function will need to accept and return a single object, not separate values.
@ -73,7 +73,7 @@ export const AddCustomSourceLogic = kea<
MakeLogicType<AddCustomSourceValues, AddCustomSourceActions, AddCustomSourceProps>
>({
connect: {
actions: [AddCustomSourceApiLogic, ['initiateCall', 'apiSuccess', ]],
actions: [AddCustomSourceApiLogic, ['makeRequest', 'apiSuccess', ]],
values: [AddCustomSourceApiLogic, ['status']],
},
path: ['enterprise_search', 'workplace_search', 'add_custom_source_logic'],
@ -85,7 +85,7 @@ export const AddCustomSourceLogic = kea<
createContentSource: () => {
const { customSourceNameValue } = values;
const { baseServiceType } = props;
actions.initiateCall({ source: customSourceNameValue, baseServiceType });
actions.makeRequest({ source: customSourceNameValue, baseServiceType });
},
addSourceSuccess: (customSource: CustomSource) => {
actions.setNewCustomSource(customSource);

View file

@ -5,11 +5,18 @@
* 2.0.
*/
import {
HealthStatus,
IndexName,
IndicesStatsIndexMetadataState,
Uuid,
} from '@elastic/elasticsearch/lib/api/types';
export interface ElasticsearchIndex {
health?: string;
status?: string;
name: string;
uuid?: string;
health?: HealthStatus;
status?: IndicesStatsIndexMetadataState;
name: IndexName;
uuid?: Uuid;
total: {
docs: {
count: number;

View file

@ -9,6 +9,8 @@ import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
import { HealthStatus } from '@elastic/elasticsearch/lib/api/types';
import {
EuiSelectable,
EuiPanel,
@ -25,10 +27,9 @@ import { EngineCreationLogic } from './engine_creation_logic';
import './search_index_selectable.scss';
export type HealthStrings = 'red' | 'green' | 'yellow' | 'unavailable';
export interface SearchIndexSelectableOption {
label: string;
health: HealthStrings;
health?: HealthStatus;
status?: string;
total: {
docs: {
@ -44,9 +45,11 @@ export interface SearchIndexSelectableOption {
const healthColorsMap = {
red: 'danger',
RED: 'danger',
green: 'success',
GREEN: 'success',
yellow: 'warning',
unavailable: '',
YELLOW: 'warning',
};
const renderIndexOption = (option: SearchIndexSelectableOption, searchValue: string) => {
@ -57,7 +60,7 @@ const renderIndexOption = (option: SearchIndexSelectableOption, searchValue: str
<EuiTextColor color="subdued">
<small>
<span className="selectableSecondaryContentLabel">
<EuiIcon type="dot" color={healthColorsMap[option.health] ?? ''} />
<EuiIcon type="dot" color={option.health ? healthColorsMap[option.health] : ''} />
&nbsp;{option.health ?? '-'}
</span>
<span className="selectableSecondaryContentLabel" data-test-subj="optionStatus">

View file

@ -10,7 +10,7 @@ import { generatePath } from 'react-router-dom';
import { ElasticsearchIndex } from '../../../../../common/types';
import { ENGINE_CRAWLER_PATH, ENGINE_PATH } from '../../routes';
import { HealthStrings, SearchIndexSelectableOption } from './search_index_selectable';
import { SearchIndexSelectableOption } from './search_index_selectable';
export const getRedirectToAfterEngineCreation = ({
ingestionMethod,
@ -38,7 +38,7 @@ export const formatIndicesToSelectable = (
return indices.map((index) => ({
...(selectedIndexName === index.name ? { checked: 'on' } : {}),
label: index.name,
health: (index.health as HealthStrings) ?? 'unavailable',
health: index.health,
status: index.status,
total: index.total,
}));

View file

@ -10,34 +10,34 @@ import { SearchIndex } from '../types';
export const searchIndices = [
{
name: 'Our API Index',
indexSlug: 'index-1',
source_type: 'API',
elasticsearch_index_name: 'ent-search-api-one',
search_engines: 'Search Engine One, Search Engine Two',
document_count: 100,
health: 'green',
data_ingestion: 'connected',
storage: '9.3mb',
},
{
name: 'Customer Feedback',
indexSlug: 'index-2',
source_type: 'Elasticsearch Index',
elasticsearch_index_name: 'es-index-two',
search_engines: 'Search Engine One',
document_count: 100,
health: 'green',
data_ingestion: 'connected',
storage: '9.3mb',
},
{
name: 'Dharma Crawler',
indexSlug: 'index-3',
source_type: 'Crawler',
elasticsearch_index_name: 'ent-search-crawler-one',
search_engines: 'Search Engine One, Search Engine Two',
document_count: 100,
health: 'yellow',
data_ingestion: 'incomplete',
storage: '9.3mb',
},
{
name: 'My Custom Source',
indexSlug: 'index-4',
source_type: 'Content Source',
elasticsearch_index_name: 'ent-search-custom-source-one',
search_engines: '--',
document_count: 1,
health: 'red',
data_ingestion: 'incomplete',
storage: '0mb',
},
] as SearchIndex[];

View file

@ -25,7 +25,6 @@ type MethodCrawlerActions = Pick<
>;
export const MethodCrawlerLogic = kea<MakeLogicType<{}, MethodCrawlerActions>>({
path: ['enterprise_search', 'method_crawler'],
connect: {
actions: [CreateCrawlerIndexApiLogic, ['apiError', 'apiSuccess', 'makeRequest']],
},
@ -34,8 +33,9 @@ export const MethodCrawlerLogic = kea<MakeLogicType<{}, MethodCrawlerActions>>({
flashAPIErrors(error);
},
apiSuccess: ({ created }) => {
KibanaLogic.values.navigateToUrl(SEARCH_INDEX_PATH.replace(':indexSlug', encodeURI(created)));
KibanaLogic.values.navigateToUrl(SEARCH_INDEX_PATH.replace(':indexName', created));
},
makeRequest: () => clearFlashMessages(),
},
path: ['enterprise_search', 'method_crawler'],
});

View file

@ -12,9 +12,7 @@
* Kibana intgegrations page
*/
import React, { useState, useEffect } from 'react';
import { useActions } from 'kea';
import React, { useState } from 'react';
import {
EuiBadge,
@ -29,7 +27,6 @@ import { i18n } from '@kbn/i18n';
import { EnterpriseSearchContentPageTemplate } from '../layout/page_template';
import { baseBreadcrumbs } from '../search_indices';
import { SearchIndicesLogic } from '../search_indices/search_indices_logic';
import { ButtonGroup, ButtonGroupOption } from './button_group';
import { SearchIndexEmptyState } from './empty_state';
@ -98,11 +95,6 @@ const METHOD_BUTTON_GROUP_OPTIONS: ButtonGroupOption[] = [
export const NewIndex: React.FC = () => {
const [selectedMethod, setSelectedMethod] = useState<ButtonGroupOption>();
const { loadSearchEngines } = useActions(SearchIndicesLogic);
useEffect(() => {
loadSearchEngines();
}, []);
return (
<EnterpriseSearchContentPageTemplate
pageChrome={[

View file

@ -12,22 +12,22 @@ import { formatApiName } from '../../utils/format_api_name';
import { DEFAULT_LANGUAGE } from './constants';
export interface NewSearchIndexValues {
rawName: string;
name: string;
language: string;
name: string;
rawName: string;
}
export interface NewSearchIndexActions {
setRawName(rawName: string): { rawName: string };
setLanguage(language: string): { language: string };
setRawName(rawName: string): { rawName: string };
}
export const NewSearchIndexLogic = kea<MakeLogicType<NewSearchIndexValues, NewSearchIndexActions>>({
path: ['enterprise_search', 'content', 'new_search_index'],
actions: {
setRawName: (rawName) => ({ rawName }),
setLanguage: (language) => ({ language }),
setRawName: (rawName) => ({ rawName }),
},
path: ['enterprise_search', 'content', 'new_search_index'],
reducers: {
language: [
DEFAULT_LANGUAGE,

View file

@ -0,0 +1,116 @@
/*
* 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, mockFlashMessageHelpers } from '../../../__mocks__/kea_logic';
import { searchIndices } from '../../__mocks__/search_indices.mock';
import { HttpError, Status } from '../../../../../common/types/api';
import { DEFAULT_META } from '../../../shared/constants';
import { IndicesAPILogic } from '../../logic/indices_api/indices_api_logic';
import { IndicesLogic } from './indices_logic';
const DEFAULT_VALUES = {
data: undefined,
indices: [],
isLoading: false,
meta: DEFAULT_META,
status: Status.IDLE,
};
describe('IndicesLogic', () => {
const { mount: apiLogicMount } = new LogicMounter(IndicesAPILogic);
const { mount } = new LogicMounter(IndicesLogic);
beforeEach(() => {
jest.clearAllMocks();
apiLogicMount();
mount();
});
it('has expected default values', () => {
expect(IndicesLogic.values).toEqual(DEFAULT_VALUES);
});
describe('actions', () => {
describe('onPaginate', () => {
it('updates meta with newPageIndex', () => {
expect(IndicesLogic.values).toEqual(DEFAULT_VALUES);
IndicesLogic.actions.onPaginate(3);
expect(IndicesLogic.values).toEqual({
...DEFAULT_VALUES,
meta: {
page: {
...DEFAULT_META.page,
current: 3,
},
},
});
});
});
});
describe('reducers', () => {
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: searchIndices, meta: newMeta });
expect(IndicesLogic.values).toEqual({
data: {
indices: searchIndices,
meta: newMeta,
},
indices: searchIndices,
isLoading: false,
meta: newMeta,
status: Status.SUCCESS,
});
});
});
});
describe('listeners', () => {
it('calls clearFlashMessages on new makeRequest', () => {
IndicesLogic.actions.makeRequest({ meta: DEFAULT_META });
expect(mockFlashMessageHelpers.clearFlashMessages).toHaveBeenCalledTimes(1);
});
it('calls flashAPIErrors on apiError', () => {
IndicesLogic.actions.apiError({} as HttpError);
expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledTimes(1);
expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledWith({});
});
});
describe('selectors', () => {
describe('indices', () => {
it('updates when apiSuccess listener triggered', () => {
expect(IndicesLogic.values).toEqual(DEFAULT_VALUES);
IndicesLogic.actions.apiSuccess({ indices: searchIndices, meta: DEFAULT_META });
expect(IndicesLogic.values).toEqual({
data: {
indices: searchIndices,
meta: DEFAULT_META,
},
indices: searchIndices,
isLoading: false,
meta: DEFAULT_META,
status: Status.SUCCESS,
});
});
});
});
});

View file

@ -0,0 +1,64 @@
/*
* 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 { Meta } from '../../../../../common/types';
import { HttpError, Status } from '../../../../../common/types/api';
import { DEFAULT_META } from '../../../shared/constants';
import { flashAPIErrors, clearFlashMessages } from '../../../shared/flash_messages';
import { updateMetaPageIndex } from '../../../shared/table_pagination';
import { IndicesAPILogic } from '../../logic/indices_api/indices_api_logic';
import { SearchIndex } from '../../types';
export interface IndicesActions {
apiError(error: HttpError): HttpError;
apiSuccess({ indices, meta }: { indices: SearchIndex[]; meta: Meta }): {
indices: SearchIndex[];
meta: Meta;
};
makeRequest: typeof IndicesAPILogic.actions.makeRequest;
onPaginate(newPageIndex: number): { newPageIndex: number };
}
export interface IndicesValues {
data: typeof IndicesAPILogic.values.data;
indices: SearchIndex[];
isLoading: boolean;
meta: Meta;
status: typeof IndicesAPILogic.values.status;
}
export const IndicesLogic = kea<MakeLogicType<IndicesValues, IndicesActions>>({
actions: { onPaginate: (newPageIndex) => ({ newPageIndex }) },
connect: {
actions: [IndicesAPILogic, ['makeRequest', 'apiSuccess', 'apiError']],
values: [IndicesAPILogic, ['data', 'status']],
},
listeners: () => ({
apiError: (e) => flashAPIErrors(e),
makeRequest: () => clearFlashMessages(),
}),
path: ['enterprise_search', 'content', 'indices_logic'],
reducers: () => ({
meta: [
DEFAULT_META,
{
apiSuccess: (_, { meta }) => meta,
onPaginate: (state, { newPageIndex }) => updateMetaPageIndex(state, newPageIndex),
},
],
}),
selectors: ({ selectors }) => ({
indices: [() => [selectors.data], (data) => data?.indices || []],
isLoading: [
() => [selectors.status],
(status) => {
return status === Status.LOADING;
},
],
}),
});

View file

@ -7,33 +7,41 @@
import '../../../__mocks__/shallow_useeffect.mock';
import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic';
import { searchIndices, searchEngines } from '../../__mocks__';
import { searchIndices } from '../../__mocks__';
import React from 'react';
import { shallow } from 'enzyme';
import { EuiBasicTable } from '@elastic/eui';
import { EuiBasicTable, 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';
import { SearchIndices } from './search_indices';
const mockValues = {
indices: searchIndices,
meta: DEFAULT_META,
};
const mockActions = {
initPage: jest.fn(),
makeRequest: jest.fn(),
onPaginate: jest.fn(),
};
describe('SearchIndices', () => {
beforeEach(() => {
jest.clearAllMocks();
global.localStorage.clear();
});
describe('Empty state', () => {
it('renders when both Search Indices and Search Engines empty', () => {
it('renders when Indices are empty', () => {
setMockValues({
searchIndices: [],
searchEngines: [],
...mockValues,
indices: [],
});
setMockActions(mockActions);
const wrapper = shallow(<SearchIndices />);
@ -44,43 +52,10 @@ describe('SearchIndices', () => {
expect(wrapper.find(GettingStartedSteps)).toHaveLength(1);
expect(wrapper.find(ElasticsearchResources)).toHaveLength(1);
});
it('renders complete empty state when only Search Indices empty', () => {
setMockValues({
searchIndices: [],
searchEngines,
});
setMockActions(mockActions);
const wrapper = shallow(<SearchIndices />);
expect(wrapper.find(AddContentEmptyPrompt)).toHaveLength(1);
expect(wrapper.find(EuiBasicTable)).toHaveLength(0);
expect(wrapper.find(GettingStartedSteps)).toHaveLength(1);
expect(wrapper.find(ElasticsearchResources)).toHaveLength(1);
});
it('renders when only Search Engines empty', () => {
setMockValues({
searchIndices,
searchEngines: [],
});
setMockActions(mockActions);
const wrapper = shallow(<SearchIndices />);
expect(wrapper.find(AddContentEmptyPrompt)).toHaveLength(0);
expect(wrapper.find(EuiBasicTable)).toHaveLength(1);
expect(wrapper.find(GettingStartedSteps)).toHaveLength(1);
expect(wrapper.find(ElasticsearchResources)).toHaveLength(1);
});
});
it('renders with Data', () => {
setMockValues({
searchIndices,
searchEngines,
});
setMockValues(mockValues);
setMockActions(mockActions);
const wrapper = shallow(<SearchIndices />);
@ -91,6 +66,39 @@ describe('SearchIndices', () => {
expect(wrapper.find(GettingStartedSteps)).toHaveLength(0);
expect(wrapper.find(ElasticsearchResources)).toHaveLength(0);
expect(mockActions.initPage).toHaveBeenCalledTimes(1);
expect(mockActions.makeRequest).toHaveBeenCalledTimes(1);
expect(wrapper.find(EuiCallOut)).toHaveLength(1);
});
it('dismisses callout on click to button', () => {
setMockValues(mockValues);
setMockActions(mockActions);
const wrapper = shallow(<SearchIndices />);
const dismissButton = wrapper.find(EuiCallOut).find(EuiButton);
expect(global.localStorage.getItem('enterprise-search-indices-callout-dismissed')).toBe(
'false'
);
dismissButton.simulate('click');
expect(global.localStorage.getItem('enterprise-search-indices-callout-dismissed')).toBe('true');
});
it('sets table pagination correctly', () => {
setMockValues(mockValues);
setMockActions(mockActions);
const wrapper = shallow(<SearchIndices />);
const table = wrapper.find(EuiBasicTable);
expect(table.prop('pagination')).toEqual({
pageIndex: 0,
pageSize: 10,
showPerPageOptions: false,
totalItemCount: 0,
});
table.simulate('change', { page: { index: 2 } });
expect(mockActions.onPaginate).toHaveBeenCalledTimes(1);
expect(mockActions.onPaginate).toHaveBeenCalledWith(3); // API's are 1 indexed, but table is 0 indexed
});
});

View file

@ -14,24 +14,36 @@ import { useValues, useActions } from 'kea';
import {
EuiBasicTable,
EuiButton,
EuiBadge,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiSpacer,
EuiTitle,
HorizontalAlignment,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { AddContentEmptyPrompt } from '../../../shared/add_content_empty_prompt';
import { ElasticsearchResources } from '../../../shared/elasticsearch_resources';
import { GettingStartedSteps } from '../../../shared/getting_started_steps';
import { EuiLinkTo, EuiButtonIconTo } from '../../../shared/react_router_helpers';
import { convertMetaToPagination, handlePageChange } from '../../../shared/table_pagination';
import { useLocalStorage } from '../../../shared/use_local_storage';
import { NEW_INDEX_PATH, SEARCH_INDEX_PATH } from '../../routes';
import { SearchIndex } from '../../types';
import { EnterpriseSearchContentPageTemplate } from '../layout/page_template';
import { SearchIndicesLogic } from './search_indices_logic';
import { IndicesLogic } from './indices_logic';
const healthColorsMap = {
green: 'success',
red: 'danger',
unavailable: '',
yellow: 'warning',
};
export const baseBreadcrumbs = [
i18n.translate('xpack.enterpriseSearch.content.searchIndices.content.breadcrumb', {
@ -43,93 +55,99 @@ export const baseBreadcrumbs = [
];
export const SearchIndices: React.FC = () => {
const { initPage, searchEnginesLoadSuccess, searchIndicesLoadSuccess } =
useActions(SearchIndicesLogic);
const { searchIndices, searchEngines } = useValues(SearchIndicesLogic);
const { makeRequest, onPaginate } = useActions(IndicesLogic);
const { meta, indices, isLoading } = useValues(IndicesLogic);
const [calloutDismissed, setCalloutDismissed] = useLocalStorage<boolean>(
'enterprise-search-indices-callout-dismissed',
false
);
useEffect(() => {
initPage();
}, []);
makeRequest({ meta });
}, [meta.page.current]);
// TODO This is for easy testing until we have the backend, please remove this before the release
// @ts-ignore
window.contentActions = {
initPage,
searchIndicesLoadSuccess,
searchEnginesLoadSuccess,
};
// TODO: Replace with a real list of indices
const columns = [
{
field: 'name',
name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.name.columnTitle', {
defaultMessage: 'Search index name',
defaultMessage: 'Index name',
}),
sortable: true,
truncateText: true,
render: (name: string, { indexSlug }: SearchIndex) => (
render: (name: string) => (
<EuiLinkTo
data-test-subj="search-index-link"
to={generatePath(SEARCH_INDEX_PATH, { indexSlug })}
to={generatePath(SEARCH_INDEX_PATH, { indexName: name })}
>
{name}
</EuiLinkTo>
),
},
{
field: 'source_type',
name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.sourceType.columnTitle', {
defaultMessage: 'Source type',
}),
sortable: true,
truncateText: true,
},
{
field: 'elasticsearch_index_name',
name: i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.elasticsearchIndexName.columnTitle',
{
defaultMessage: 'Elasticsearch index name',
}
),
sortable: true,
truncateText: true,
},
{
field: 'search_engines',
name: i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.searchEngines.columnTitle',
{
defaultMessage: 'Attached search engines',
}
),
truncateText: true,
},
{
field: 'document_count',
field: 'total.docs.count',
name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.docsCount.columnTitle', {
defaultMessage: 'Documents',
defaultMessage: 'Docs count',
}),
sortable: true,
truncateText: true,
align: 'right' as HorizontalAlignment,
},
{
name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.actions.columnTitle', {
defaultMessage: 'Actions',
field: 'health',
name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.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,
},
{
field: 'data_ingestion',
name: i18n.translate(
'xpack.enterpriseSearch.content.searchIndices.dataIngestion.columnTitle',
{
defaultMessage: 'Data ingestion',
}
),
render: (dataIngestionStatus: string) =>
dataIngestionStatus ? (
<EuiBadge color={dataIngestionStatus === 'connected' ? 'success' : 'warning'}>
{dataIngestionStatus}
</EuiBadge>
) : null,
truncateText: true,
},
{
align: 'right' as HorizontalAlignment,
field: 'total.store.size_in_bytes',
name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.storage.columnTitle', {
defaultMessage: 'Storage',
}),
sortable: true,
truncateText: true,
},
{
actions: [
{
render: ({ indexSlug }: SearchIndex) => (
render: ({ name }: SearchIndex) => (
<EuiButtonIconTo
iconType="eye"
data-test-subj="view-search-index-button"
to={generatePath(SEARCH_INDEX_PATH, { indexSlug })}
to={generatePath(SEARCH_INDEX_PATH, {
indexName: name,
})}
/>
),
},
],
name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.actions.columnTitle', {
defaultMessage: 'Actions',
}),
},
];
@ -155,7 +173,7 @@ export const SearchIndices: React.FC = () => {
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem>
<GettingStartedSteps step={searchIndices.length === 0 ? 'first' : 'second'} />
<GettingStartedSteps step={indices.length === 0 ? 'first' : 'second'} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ElasticsearchResources />
@ -165,7 +183,7 @@ export const SearchIndices: React.FC = () => {
);
const pageTitle =
searchIndices.length !== 0
indices.length !== 0
? i18n.translate('xpack.enterpriseSearch.content.searchIndices.searchIndices.pageTitle', {
defaultMessage: 'Content',
})
@ -187,7 +205,7 @@ export const SearchIndices: React.FC = () => {
rightSideItems: [createNewIndexButton],
}}
>
{searchIndices.length !== 0 ? (
{indices.length !== 0 || isLoading ? (
<>
<EuiTitle>
<h2>
@ -200,13 +218,54 @@ export const SearchIndices: React.FC = () => {
</h2>
</EuiTitle>
<EuiSpacer size="l" />
<EuiBasicTable items={searchIndices} columns={columns} />
{!calloutDismissed && (
<EuiCallOut
size="m"
title={i18n.translate('xpack.enterpriseSearch.content.callout.title', {
defaultMessage: 'Introducing Elasticsearch indices in Enterprise Search',
})}
iconType="iInCircle"
>
<p>
<FormattedMessage
id="xpack.enterpriseSearch.content.indices.callout.text"
defaultMessage="Your Elasticsearch indices are now front and center in Enterprise Search. You can create new indices and build search experiences with them directly. To learn more about how to use Elasticsearch indices in Enterprise Search {docLink}"
values={{
docLink: (
<EuiLinkTo data-test-subj="search-index-link" to="#">
{i18n.translate(
'xpack.enterpriseSearch.content.indices.callout.docLink',
{
defaultMessage: 'read the documentation',
}
)}
</EuiLinkTo>
),
}}
/>
</p>
<EuiButton fill onClick={() => setCalloutDismissed(true)}>
{i18n.translate('xpack.enterpriseSearch.content.callout.dismissButton', {
defaultMessage: 'Dismiss',
})}
</EuiButton>
</EuiCallOut>
)}
<EuiSpacer size="l" />
<EuiBasicTable
items={indices}
columns={columns}
onChange={handlePageChange(onPaginate)}
pagination={{ ...convertMetaToPagination(meta), showPerPageOptions: false }}
tableLayout="auto"
loading={isLoading}
/>
</>
) : (
<AddContentEmptyPrompt />
)}
<EuiSpacer size="xxl" />
{(searchEngines.length === 0 || searchIndices.length === 0) && engineSteps}
{indices.length === 0 && !isLoading && engineSteps}
</EnterpriseSearchContentPageTemplate>
)
</>

View file

@ -1,55 +0,0 @@
/*
* 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 { searchIndices, searchEngines } from '../../__mocks__';
import { SearchIndicesLogic } from './search_indices_logic';
describe('SearchIndicesLogic', () => {
const { mount } = new LogicMounter(SearchIndicesLogic);
const DEFAULT_VALUES = {
searchEngines: [],
searchIndices: [],
};
beforeEach(() => {
jest.clearAllMocks();
mount();
});
it('has expected default values', () => {
expect(SearchIndicesLogic.values).toEqual(DEFAULT_VALUES);
});
describe('actions', () => {
describe('searchIndicesLoadSuccess', () => {
it('should set searchIndices', () => {
SearchIndicesLogic.actions.searchIndicesLoadSuccess(searchIndices);
expect(SearchIndicesLogic.values).toEqual({
...DEFAULT_VALUES,
searchIndices,
});
});
});
describe('searchEnginesLoadSuccess', () => {
it('should set searchEngines', () => {
SearchIndicesLogic.actions.searchEnginesLoadSuccess(searchEngines);
expect(SearchIndicesLogic.values).toEqual({
...DEFAULT_VALUES,
searchEngines,
});
});
});
});
describe.skip('listeners', () => {
describe('loadSearchEngines', () => {});
describe('loadSearchIndices', () => {});
});
});

View file

@ -1,79 +0,0 @@
/*
* 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 {
searchIndices as searchIndicesMock,
searchEngines as searchEnginesMock,
} from '../../__mocks__';
import { kea, MakeLogicType } from 'kea';
import { Engine } from '../../../app_search/components/engine/types';
import { flashAPIErrors } from '../../../shared/flash_messages';
import { SearchIndex } from '../../types';
export interface SearchIndicesValues {
searchIndices: SearchIndex[];
searchEngines: Engine[];
}
export interface SearchIndicesActions {
initPage(): void;
loadSearchEngines(): void;
searchEnginesLoadSuccess(searchEngines: Engine[]): Engine[]; // TODO proper types when backend ready
loadSearchIndices(): void;
searchIndicesLoadSuccess(searchIndices: SearchIndex[]): SearchIndex[]; // TODO proper types when backend ready
}
export const SearchIndicesLogic = kea<MakeLogicType<SearchIndicesValues, SearchIndicesActions>>({
path: ['enterprise_search', 'content', 'search_indices'],
actions: {
initPage: true,
loadSearchIndices: true,
searchIndicesLoadSuccess: (searchIndices) => searchIndices,
loadSearchEngines: true,
searchEnginesLoadSuccess: (searchEngines) => searchEngines,
},
reducers: {
searchIndices: [
[],
{
searchIndicesLoadSuccess: (_, searchIndices) => searchIndices,
},
],
searchEngines: [
[],
{
searchEnginesLoadSuccess: (_, searchEngines) => searchEngines,
},
],
},
listeners: ({ actions }) => ({
initPage: async () => {
actions.loadSearchEngines();
actions.loadSearchIndices();
},
loadSearchEngines: async () => {
try {
// TODO replace with actual backend call, add test cases
const response = await Promise.resolve(searchEnginesMock);
actions.searchEnginesLoadSuccess(response);
} catch (e) {
flashAPIErrors(e);
}
},
loadSearchIndices: async () => {
try {
// TODO replace with actual backend call, add test cases
const response = await Promise.resolve(searchIndicesMock);
actions.searchIndicesLoadSuccess(response);
} catch (e) {
flashAPIErrors(e);
}
},
}),
});

View file

@ -0,0 +1,27 @@
/*
* 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 { Meta } from '../../../../../common/types';
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';
import { SearchIndex } from '../../types';
export const indicesApi = async ({ meta }: { meta: Meta }) => {
const { http } = HttpLogic.values;
const route = '/internal/enterprise_search/indices';
const query = {
page: meta.page.current,
size: meta.page.size,
};
const response = await http.get<{ indices: SearchIndex[]; meta: Meta }>(route, {
query,
});
return response;
};
export const IndicesAPILogic = createApiLogic(['content', 'indices_api_logic'], indicesApi);

View file

@ -9,11 +9,14 @@
* As of 2022-04-04, this shape is still in debate. Specifically, the `source_type` will be changing as we get closer to 8.3.
* These merely serve as placeholders for static data for now.
*/
import { HealthStatus } from '@elastic/elasticsearch/lib/api/types';
export interface SearchIndex {
name: string;
indexSlug: string;
source_type: string;
elasticsearch_index_name: string;
search_engines: string;
document_count: number;
health: HealthStatus;
data_ingestion: 'connected' | 'incomplete';
storage: string;
}

View file

@ -42,10 +42,19 @@ describe('CreateApiLogic', () => {
logic.actions.makeRequest({});
expect(logic.values).toEqual({
...DEFAULT_VALUES,
apiStatus: { status: Status.LOADING },
status: Status.LOADING,
});
});
it('should set persist data in between new requests', () => {
logic.actions.apiSuccess(123);
logic.actions.makeRequest({});
expect(logic.values).toEqual({
...DEFAULT_VALUES,
apiStatus: { data: 123, status: Status.LOADING },
data: 123,
status: Status.LOADING,
apiStatus: {
status: Status.LOADING,
},
});
});
});
@ -54,12 +63,12 @@ describe('CreateApiLogic', () => {
logic.actions.apiSuccess({ success: 'data' });
expect(logic.values).toEqual({
...DEFAULT_VALUES,
status: Status.SUCCESS,
data: { success: 'data' },
apiStatus: {
status: Status.SUCCESS,
data: { success: 'data' },
status: Status.SUCCESS,
},
data: { success: 'data' },
status: Status.SUCCESS,
});
});
});
@ -68,14 +77,14 @@ describe('CreateApiLogic', () => {
logic.actions.apiError('error' as any as HttpError);
expect(logic.values).toEqual({
...DEFAULT_VALUES,
status: Status.ERROR,
data: undefined,
error: 'error',
apiStatus: {
status: Status.ERROR,
data: undefined,
error: 'error',
status: Status.ERROR,
},
data: undefined,
error: 'error',
status: Status.ERROR,
});
});
});
@ -84,14 +93,14 @@ describe('CreateApiLogic', () => {
logic.actions.apiError('error' as any as HttpError);
expect(logic.values).toEqual({
...DEFAULT_VALUES,
status: Status.ERROR,
data: undefined,
error: 'error',
apiStatus: {
status: Status.ERROR,
data: undefined,
error: 'error',
status: Status.ERROR,
},
data: undefined,
error: 'error',
status: Status.ERROR,
});
logic.actions.apiReset();
expect(logic.values).toEqual(DEFAULT_VALUES);
@ -115,14 +124,19 @@ describe('CreateApiLogic', () => {
const apiSuccessMock = jest.spyOn(logic.actions, 'apiSuccess');
const apiErrorMock = jest.spyOn(logic.actions, 'apiError');
apiCallMock.mockReturnValue(
Promise.reject({ body: { statusCode: 404, message: 'message' } })
Promise.reject({
body: {
message: 'message',
statusCode: 404,
},
})
);
logic.actions.makeRequest({ arg: 'argument1' });
expect(apiCallMock).toHaveBeenCalledWith({ arg: 'argument1' });
await nextTick();
expect(apiSuccessMock).not.toHaveBeenCalled();
expect(apiErrorMock).toHaveBeenCalledWith({
body: { statusCode: 404, message: 'message' },
body: { message: 'message', statusCode: 404 },
});
});
});

View file

@ -11,16 +11,16 @@ import { ApiStatus, Status, HttpError } from '../../../../common/types/api';
export interface Values<T> {
apiStatus: ApiStatus<T>;
status: Status;
data?: T;
error: HttpError;
status: Status;
}
export interface Actions<Args, Result> {
makeRequest(args: Args): Args;
apiError(error: HttpError): HttpError;
apiSuccess(result: Result): Result;
apiReset(): void;
apiSuccess(result: Result): Result;
makeRequest(args: Args): Args;
}
export const createApiLogic = <Result, Args>(
@ -28,36 +28,12 @@ export const createApiLogic = <Result, Args>(
apiFunction: (args: Args) => Promise<Result>
) =>
kea<MakeLogicType<Values<Result>, Actions<Args, Result>>>({
path: ['enterprise_search', ...path],
actions: {
makeRequest: (args) => args,
apiError: (error) => error,
apiSuccess: (result) => result,
apiReset: true,
apiSuccess: (result) => result,
makeRequest: (args) => args,
},
reducers: () => ({
apiStatus: [
{
status: Status.IDLE,
},
{
makeRequest: () => ({
status: Status.LOADING,
}),
apiError: (_, error) => ({
status: Status.ERROR,
error,
}),
apiSuccess: (_, data) => ({
status: Status.SUCCESS,
data,
}),
apiReset: () => ({
status: Status.IDLE,
}),
},
],
}),
listeners: ({ actions }) => ({
makeRequest: async (args) => {
try {
@ -68,9 +44,34 @@ export const createApiLogic = <Result, Args>(
}
},
}),
path: ['enterprise_search', ...path],
reducers: () => ({
apiStatus: [
{
status: Status.IDLE,
},
{
apiError: (_, error) => ({
error,
status: Status.ERROR,
}),
apiReset: () => ({ status: Status.IDLE }),
apiSuccess: (_, data) => ({
data,
status: Status.SUCCESS,
}),
makeRequest: ({ data }) => {
return {
data,
status: Status.LOADING,
};
},
},
],
}),
selectors: ({ selectors }) => ({
status: [() => [selectors.apiStatus], (apiStatus: ApiStatus<Result>) => apiStatus.status],
data: [() => [selectors.apiStatus], (apiStatus: ApiStatus<Result>) => apiStatus.data],
error: [() => [selectors.apiStatus], (apiStatus: ApiStatus<Result>) => apiStatus.error],
status: [() => [selectors.apiStatus], (apiStatus: ApiStatus<Result>) => apiStatus.status],
}),
});

View file

@ -36,7 +36,7 @@ export function registerIndexRoutes({ router }: RouteDependencies) {
path: '/internal/enterprise_search/indices',
validate: {
query: schema.object({
page: schema.number({ defaultValue: 1, min: 0 }),
page: schema.number({ defaultValue: 0, min: 0 }),
size: schema.number({ defaultValue: 10, min: 0 }),
}),
},
@ -57,8 +57,8 @@ export function registerIndexRoutes({ router }: RouteDependencies) {
page: {
current: page,
size,
totalPages,
totalResults,
total_pages: totalPages,
total_results: totalResults,
},
},
},

View file

@ -11702,15 +11702,12 @@
"xpack.enterpriseSearch.content.searchIndices.content.breadcrumb": "Contenu",
"xpack.enterpriseSearch.content.searchIndices.create.buttonTitle": "Créer un nouvel index",
"xpack.enterpriseSearch.content.searchIndices.docsCount.columnTitle": "Documents",
"xpack.enterpriseSearch.content.searchIndices.elasticsearchIndexName.columnTitle": "Nom de l'index Elasticsearch",
"xpack.enterpriseSearch.content.searchIndices.name.columnTitle": "Nom de l'index de recherche",
"xpack.enterpriseSearch.content.searchIndices.searchEngines.columnTitle": "Moteurs de recherche attachés",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.breadcrumb": "Rechercher dans les index",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.emptyPageTitle": "Bienvenue dans Enterprise Search",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.pageTitle": "Contenu",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.stepsTitle": "Créer de belles expériences de recherche avec Enterprise Search",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.tableTitle": "Rechercher dans les index",
"xpack.enterpriseSearch.content.searchIndices.sourceType.columnTitle": "Type de source",
"xpack.enterpriseSearch.curations.settings.licenseUpgradeLink": "En savoir plus sur les mises à niveau incluses dans la licence",
"xpack.enterpriseSearch.curations.settings.start30DayTrialButtonLabel": "Démarrer un essai gratuit de 30 jours",
"xpack.enterpriseSearch.elasticsearch.nav.contentTitle": "Elasticsearch",

View file

@ -11693,15 +11693,12 @@
"xpack.enterpriseSearch.content.searchIndices.content.breadcrumb": "コンテンツ",
"xpack.enterpriseSearch.content.searchIndices.create.buttonTitle": "新しいインデックスを作成",
"xpack.enterpriseSearch.content.searchIndices.docsCount.columnTitle": "ドキュメント",
"xpack.enterpriseSearch.content.searchIndices.elasticsearchIndexName.columnTitle": "Elasticsearchインデックス名",
"xpack.enterpriseSearch.content.searchIndices.name.columnTitle": "検索インデックス名",
"xpack.enterpriseSearch.content.searchIndices.searchEngines.columnTitle": "接続された検索エンジン",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.breadcrumb": "インデックスの検索",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.emptyPageTitle": "エンタープライズ サーチへようこそ",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.pageTitle": "コンテンツ",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.stepsTitle": "エンタープライズ サーチで構築する優れた検索エクスペリエンス",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.tableTitle": "インデックスの検索",
"xpack.enterpriseSearch.content.searchIndices.sourceType.columnTitle": "ソースタイプ",
"xpack.enterpriseSearch.curations.settings.licenseUpgradeLink": "ライセンスアップグレードの詳細",
"xpack.enterpriseSearch.curations.settings.start30DayTrialButtonLabel": "30 日間のトライアルの開始",
"xpack.enterpriseSearch.elasticsearch.nav.contentTitle": "Elasticsearch",

View file

@ -11708,15 +11708,12 @@
"xpack.enterpriseSearch.content.searchIndices.content.breadcrumb": "内容",
"xpack.enterpriseSearch.content.searchIndices.create.buttonTitle": "创建新索引",
"xpack.enterpriseSearch.content.searchIndices.docsCount.columnTitle": "文档",
"xpack.enterpriseSearch.content.searchIndices.elasticsearchIndexName.columnTitle": "Elasticsearch 索引名称",
"xpack.enterpriseSearch.content.searchIndices.name.columnTitle": "搜索索引名称",
"xpack.enterpriseSearch.content.searchIndices.searchEngines.columnTitle": "已附加搜索引擎",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.breadcrumb": "搜索索引",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.emptyPageTitle": "欢迎使用 Enterprise Search",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.pageTitle": "内容",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.stepsTitle": "通过 Enterprise Search 构建出色的搜索体验",
"xpack.enterpriseSearch.content.searchIndices.searchIndices.tableTitle": "搜索索引",
"xpack.enterpriseSearch.content.searchIndices.sourceType.columnTitle": "源类型",
"xpack.enterpriseSearch.curations.settings.licenseUpgradeLink": "详细了解许可证升级",
"xpack.enterpriseSearch.curations.settings.start30DayTrialButtonLabel": "开始为期 30 天的试用",
"xpack.enterpriseSearch.elasticsearch.nav.contentTitle": "Elasticsearch",