diff --git a/x-pack/platform/plugins/private/translations/translations/de-DE.json b/x-pack/platform/plugins/private/translations/translations/de-DE.json index f54995544d99b..9594b27e7b45a 100644 --- a/x-pack/platform/plugins/private/translations/translations/de-DE.json +++ b/x-pack/platform/plugins/private/translations/translations/de-DE.json @@ -18473,8 +18473,6 @@ "xpack.idxMgmt.createIndex.modal.indexMode.label": "Indexmodus", "xpack.idxMgmt.createIndex.modal.indexName.label": "Indexname", "xpack.idxMgmt.createIndex.modal.invalidName.error": "Der Indexname ist nicht gültig", - "xpack.idxMgmt.createIndex.modal.saveButton": "Erstellen", - "xpack.idxMgmt.createIndex.modal.title": "Index erstellen", "xpack.idxMgmt.createIndex.successfullyCreatedIndexMessage": "Index erfolgreich erstellt: {indexName}", "xpack.idxMgmt.createRoute.duplicateTemplateIdErrorMessage": "Es gibt bereits eine Vorlage mit dem Namen \"{name}“.", "xpack.idxMgmt.createTemplate.cloneTemplatePageTitle": "Klonen Sie die Vorlage „{name}“", diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index e6f9e2c3cd02f..01894f87e8750 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -18735,8 +18735,6 @@ "xpack.idxMgmt.createIndex.modal.indexMode.label": "Mode d'index", "xpack.idxMgmt.createIndex.modal.indexName.label": "Nom de l'index", "xpack.idxMgmt.createIndex.modal.invalidName.error": "Le nom de l'index n'est pas valide", - "xpack.idxMgmt.createIndex.modal.saveButton": "Créer", - "xpack.idxMgmt.createIndex.modal.title": "Créer un index", "xpack.idxMgmt.createIndex.successfullyCreatedIndexMessage": "Création réussie de l'index : {indexName}", "xpack.idxMgmt.createRoute.duplicateTemplateIdErrorMessage": "Un modèle s'appelle déjà \"{name}\".", "xpack.idxMgmt.createTemplate.cloneTemplatePageTitle": "Cloner le modèle \"{name}\"", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 8c92ee1f9cd87..157b9918b72b2 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -18759,8 +18759,6 @@ "xpack.idxMgmt.createIndex.modal.indexMode.label": "インデックスモード", "xpack.idxMgmt.createIndex.modal.indexName.label": "インデックス名", "xpack.idxMgmt.createIndex.modal.invalidName.error": "インデックス名が有効ではありません", - "xpack.idxMgmt.createIndex.modal.saveButton": "作成", - "xpack.idxMgmt.createIndex.modal.title": "インデックスの作成", "xpack.idxMgmt.createIndex.successfullyCreatedIndexMessage": "インデックスの作成が正常に完了しました:{indexName}", "xpack.idxMgmt.createRoute.duplicateTemplateIdErrorMessage": "''{name}''という名前のテンプレートがすでに存在します。", "xpack.idxMgmt.createTemplate.cloneTemplatePageTitle": "テンプレート''{name}''の複製", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 737e221088681..1f5e573a8fa62 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -18749,8 +18749,6 @@ "xpack.idxMgmt.createIndex.modal.indexMode.label": "索引模式", "xpack.idxMgmt.createIndex.modal.indexName.label": "索引名称", "xpack.idxMgmt.createIndex.modal.invalidName.error": "索引名称无效", - "xpack.idxMgmt.createIndex.modal.saveButton": "创建", - "xpack.idxMgmt.createIndex.modal.title": "创建索引", "xpack.idxMgmt.createIndex.successfullyCreatedIndexMessage": "已成功创建索引:{indexName}", "xpack.idxMgmt.createRoute.duplicateTemplateIdErrorMessage": "已有名称为“{name}”的模板。", "xpack.idxMgmt.createTemplate.cloneTemplatePageTitle": "克隆模板“{name}”", diff --git a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/helpers/actions/index_table_actions.ts b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/helpers/actions/index_table_actions.ts index 561c4990bc7d8..91e9e2be2d72b 100644 --- a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/helpers/actions/index_table_actions.ts +++ b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/helpers/actions/index_table_actions.ts @@ -117,13 +117,8 @@ export const createCreateIndexActions = () => { }; const clickCreateIndexSaveButton = async () => { - // `CreateIndexModal` renders the submit button in the modal footer and wires it via `form=...` - // (instead of nesting it inside the
). In JSDOM, clicking a submit button triggers - // `requestSubmit`, which throws "Not implemented". We only need the React `onClick` handler, - // so switch the DOM button type to avoid the native submit behavior. - const saveButton = screen.getByTestId('createIndexSaveButton') as HTMLButtonElement; - saveButton.type = 'button'; - fireEvent.click(saveButton); + const form = screen.getByTestId('createIndexModalForm'); + fireEvent.submit(form); }; const setIndexName = (name: string) => { diff --git a/x-pack/platform/plugins/shared/index_management/common/constants/index.ts b/x-pack/platform/plugins/shared/index_management/common/constants/index.ts index d0265abc92b0a..7104c11242d3d 100644 --- a/x-pack/platform/plugins/shared/index_management/common/constants/index.ts +++ b/x-pack/platform/plugins/shared/index_management/common/constants/index.ts @@ -68,6 +68,4 @@ export const PLUGIN = { export const MAX_DOCUMENTS_FOR_CONVERT_TO_LOOKUP_INDEX = 2000000000; // 2 billion documents export const MAX_SHARDS_FOR_CONVERT_TO_LOOKUP_INDEX = 1; // Single shard -export const PLATFORM_INDEX_MGMT_V2 = 'platform:indexManagementV2'; - export const DEFAULT_DOCUMENT_PAGE_SIZE = 10; diff --git a/x-pack/platform/plugins/shared/index_management/public/application/hooks/use_is_platform_index_management_v2_enabled.ts b/x-pack/platform/plugins/shared/index_management/public/application/hooks/use_is_platform_index_management_v2_enabled.ts deleted file mode 100644 index b73be5ea1034f..0000000000000 --- a/x-pack/platform/plugins/shared/index_management/public/application/hooks/use_is_platform_index_management_v2_enabled.ts +++ /dev/null @@ -1,14 +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 { PLATFORM_INDEX_MGMT_V2 } from '../../../common/constants'; -import { useAppContext } from '../app_context'; - -export const useIsPlatformIndexManagementV2Enabled = (): boolean => { - const { settings } = useAppContext(); - return settings.client.get(PLATFORM_INDEX_MGMT_V2, false); -}; diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_button.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_button.tsx index f1683a144ffa4..aa87a8ecb2a34 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_button.tsx +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_button.tsx @@ -12,9 +12,7 @@ import { EuiButton } from '@elastic/eui'; import useObservable from 'react-use/lib/useObservable'; import { CreateIndexModal } from './create_index_modal'; -import { CreateIndexModalV2 } from './create_index_modal_v2'; import { useAppContext } from '../../../../app_context'; -import { useIsPlatformIndexManagementV2Enabled } from '../../../../hooks/use_is_platform_index_management_v2_enabled'; export interface CreateIndexButtonProps { loadIndices: () => void; @@ -26,12 +24,9 @@ export const CreateIndexButton = ({ loadIndices, share, dataTestSubj }: CreateIn const { core: { chrome }, } = useAppContext(); - const isPlatformIndexManagementV2Enabled = useIsPlatformIndexManagementV2Enabled(); const [createIndexModalOpen, setCreateIndexModalOpen] = useState(false); const createIndexUrl = share?.url.locators.get('SEARCH_CREATE_INDEX')?.useUrl({}); - const IndexModal = isPlatformIndexManagementV2Enabled ? CreateIndexModalV2 : CreateIndexModal; - const activeSolutionId = useObservable(chrome.getActiveSolutionNavId$()); const actionProp = @@ -55,7 +50,10 @@ export const CreateIndexButton = ({ loadIndices, share, dataTestSubj }: CreateIn /> {createIndexModalOpen && ( - setCreateIndexModalOpen(false)} loadIndices={loadIndices} /> + setCreateIndexModalOpen(false)} + loadIndices={loadIndices} + /> )} ); diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_modal.test.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_modal.test.tsx new file mode 100644 index 0000000000000..aded9fe3a5270 --- /dev/null +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_modal.test.tsx @@ -0,0 +1,183 @@ +/* + * 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 { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { EuiThemeProvider } from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n-react'; +import { CreateIndexModal } from './create_index_modal'; + +const mockCreateIndex = jest.fn(); +jest.mock('../../../../services', () => ({ + createIndex: (...args: unknown[]) => mockCreateIndex(...args), +})); + +const mockShowSuccessToast = jest.fn(); +jest.mock('../../../../services/notification', () => ({ + notificationService: { + showSuccessToast: (...args: unknown[]) => mockShowSuccessToast(...args), + }, +})); + +jest.mock('./utils', () => ({ + generateRandomIndexName: () => 'search-abcd', + isValidIndexName: (name: string) => { + if (!name || name !== name.toLowerCase() || name.length === 0) return false; + return true; + }, +})); + +const renderModal = (props: Partial> = {}) => { + const defaultProps = { + closeModal: jest.fn(), + loadIndices: jest.fn(), + }; + + return render( + + + + + + ); +}; + +describe('CreateIndexModal', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the modal with title and description', () => { + renderModal(); + expect(screen.getByText('Create your index')).toBeInTheDocument(); + expect( + screen.getByText('An index stores and defines the schema of your data.') + ).toBeInTheDocument(); + }); + + it('pre-fills the index name with a generated random name', () => { + renderModal(); + const input = screen.getByTestId('createIndexNameFieldText'); + expect(input).toHaveValue('search-abcd'); + }); + + it('shows index name and index mode form fields', () => { + renderModal(); + expect(screen.getByTestId('createIndexNameFieldText')).toBeInTheDocument(); + expect(screen.getByTestId('indexModeField')).toBeInTheDocument(); + }); + + it('shows a validation error for invalid index names', async () => { + renderModal(); + const input = screen.getByTestId('createIndexNameFieldText'); + await userEvent.clear(input); + await userEvent.type(input, 'INVALID'); + + expect(screen.getByText('Index name is not valid')).toBeInTheDocument(); + }); + + it('clears validation error when a valid name is entered', async () => { + renderModal(); + const input = screen.getByTestId('createIndexNameFieldText'); + + await userEvent.clear(input); + await userEvent.type(input, 'INVALID'); + expect(screen.getByText('Index name is not valid')).toBeInTheDocument(); + + await userEvent.clear(input); + await userEvent.type(input, 'valid-name'); + expect(screen.queryByText('Index name is not valid')).not.toBeInTheDocument(); + }); + + it('calls closeModal when cancel button is clicked', () => { + const closeModal = jest.fn(); + renderModal({ closeModal }); + fireEvent.click(screen.getByTestId('createIndexCancelButton')); + expect(closeModal).toHaveBeenCalled(); + }); + + it('toggles the API code block when Show/Hide API button is clicked', () => { + renderModal(); + expect(screen.getByText('Show API')).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('createIndexShowApiButton')); + expect(screen.getByText('Hide API')).toBeInTheDocument(); + expect(screen.getByText(/PUT search-abcd/)).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('createIndexShowApiButton')); + expect(screen.getByText('Show API')).toBeInTheDocument(); + }); + + it('reflects the selected index mode in the API code block', async () => { + renderModal(); + fireEvent.click(screen.getByTestId('createIndexShowApiButton')); + + expect(screen.getByText(/\"mode\":\"standard\"/)).toBeInTheDocument(); + }); + + it('creates the index on submit with valid name', async () => { + const closeModal = jest.fn(); + const loadIndices = jest.fn(); + mockCreateIndex.mockResolvedValue({ error: undefined }); + + renderModal({ closeModal, loadIndices }); + fireEvent.click(screen.getByTestId('createIndexSaveButton')); + + await waitFor(() => { + expect(mockCreateIndex).toHaveBeenCalledWith('search-abcd', 'standard'); + }); + + await waitFor(() => { + expect(mockShowSuccessToast).toHaveBeenCalled(); + expect(closeModal).toHaveBeenCalled(); + expect(loadIndices).toHaveBeenCalled(); + }); + }); + + it('creates the index when pressing Enter in the index name input', async () => { + const closeModal = jest.fn(); + const loadIndices = jest.fn(); + mockCreateIndex.mockResolvedValue({ error: undefined }); + + renderModal({ closeModal, loadIndices }); + fireEvent.submit(screen.getByTestId('createIndexModalForm')); + + await waitFor(() => { + expect(mockCreateIndex).toHaveBeenCalledWith('search-abcd', 'standard'); + }); + + await waitFor(() => { + expect(mockShowSuccessToast).toHaveBeenCalled(); + expect(closeModal).toHaveBeenCalled(); + expect(loadIndices).toHaveBeenCalled(); + }); + }); + + it('displays an error callout when index creation fails', async () => { + mockCreateIndex.mockResolvedValue({ error: { message: 'Index already exists' } }); + + renderModal(); + fireEvent.click(screen.getByTestId('createIndexSaveButton')); + + await waitFor(() => { + expect(screen.getByText('Error creating index')).toBeInTheDocument(); + expect(screen.getByText(/Index already exists/)).toBeInTheDocument(); + }); + }); + + it('does not submit when the index name is invalid', async () => { + renderModal(); + const input = screen.getByTestId('createIndexNameFieldText'); + + await userEvent.clear(input); + + fireEvent.click(screen.getByTestId('createIndexSaveButton')); + + expect(mockCreateIndex).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_modal.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_modal.tsx index 8b53d4bc8e2e0..63074fca07964 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_modal.tsx +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_modal.tsx @@ -8,6 +8,7 @@ import React, { Fragment, useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; import { EuiButton, EuiButtonEmpty, @@ -21,10 +22,15 @@ import { EuiModalHeaderTitle, EuiModalBody, EuiModalFooter, - EuiScreenReaderOnly, EuiSpacer, EuiText, useGeneratedHtmlId, + EuiFlexGroup, + EuiFlexItem, + useEuiTheme, + EuiHorizontalRule, + EuiCodeBlock, + type UseEuiTheme, } from '@elastic/eui'; import { LOOKUP_INDEX_MODE, STANDARD_INDEX_MODE } from '../../../../../../common/constants'; @@ -32,13 +38,19 @@ import { indexModeDescriptions, indexModeLabels } from '../../../../lib/index_mo import { createIndex } from '../../../../services'; import { notificationService } from '../../../../services/notification'; -import { isValidIndexName } from './utils'; +import { generateRandomIndexName, isValidIndexName } from './utils'; const INVALID_INDEX_NAME_ERROR = i18n.translate( 'xpack.idxMgmt.createIndex.modal.invalidName.error', { defaultMessage: 'Index name is not valid' } ); +const modalHeaderStyles = ({ euiTheme }: UseEuiTheme) => css` + flex-direction: column; + align-items: flex-start; + gap: ${euiTheme.size.s}; +`; + export interface CreateIndexModalProps { closeModal: () => void; loadIndices: () => void; @@ -46,14 +58,24 @@ export interface CreateIndexModalProps { export const CreateIndexModal = ({ closeModal, loadIndices }: CreateIndexModalProps) => { const modalTitleId = useGeneratedHtmlId(); + const { euiTheme } = useEuiTheme(); - const indexModeFormRowId = useGeneratedHtmlId({ prefix: 'indexMode' }); + const [indexName, setIndexName] = useState(generateRandomIndexName); - const [indexName, setIndexName] = useState(''); const [indexMode, setIndexMode] = useState(STANDARD_INDEX_MODE); const [indexNameError, setIndexNameError] = useState(); const [isSaving, setIsSaving] = useState(false); const [createError, setCreateError] = useState(); + const [showApi, setShowApi] = useState(false); + + const apiCode = `PUT ${indexName} +{ + "settings": { + "index":{ + "mode":"${indexMode}" + } + } +}`; const putCreateIndex = useCallback(async () => { setIsSaving(true); @@ -84,6 +106,11 @@ export const CreateIndexModal = ({ closeModal, loadIndices }: CreateIndexModalPr } }; + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSave(); + }; + const onNameChange = (name: string) => { setIndexName(name); if (!isValidIndexName(name)) { @@ -98,15 +125,23 @@ export const CreateIndexModal = ({ closeModal, loadIndices }: CreateIndexModalPr aria-labelledby={modalTitleId} onClose={closeModal} initialFocus="[name=indexName]" - css={{ width: 450 }} + css={css` + width: calc(${euiTheme.size.xxxl} * 16); + `} > - + + + + {createError && ( @@ -130,109 +165,144 @@ export const CreateIndexModal = ({ closeModal, loadIndices }: CreateIndexModalPr )} - - - onNameChange(e.target.value)} - data-test-subj="createIndexNameFieldText" - /> - - - {i18n.translate('xpack.idxMgmt.createIndex.modal.indexMode.label', { + + + + + onNameChange(e.target.value)} + data-test-subj="createIndexNameFieldText" + /> + + + + - {`, ${ - indexModeLabels[indexMode as keyof typeof indexModeLabels] ?? indexMode - }`} - - - } - isDisabled={isSaving} - > - setIndexMode(mode)} - data-test-subj="indexModeField" - options={[ - { - value: STANDARD_INDEX_MODE, - inputDisplay: indexModeLabels[STANDARD_INDEX_MODE], - 'data-test-subj': 'indexModeStandardOption', - dropdownDisplay: ( - - {indexModeLabels[STANDARD_INDEX_MODE]} - -

{indexModeDescriptions[STANDARD_INDEX_MODE]}

-
-
- ), - }, - { - value: LOOKUP_INDEX_MODE, - inputDisplay: indexModeLabels[LOOKUP_INDEX_MODE], - 'data-test-subj': 'indexModeLookupOption', - dropdownDisplay: ( - - {indexModeLabels[LOOKUP_INDEX_MODE]} - -

{indexModeDescriptions[LOOKUP_INDEX_MODE]}

-
-
- ), - }, - ]} - /> -
+ isDisabled={isSaving} + css={css` + width: calc(${euiTheme.size.xxxl} * 4); + `} + > + setIndexMode(mode)} + data-test-subj="indexModeField" + options={[ + { + value: STANDARD_INDEX_MODE, + inputDisplay: indexModeLabels[STANDARD_INDEX_MODE], + 'data-test-subj': 'indexModeStandardOption', + dropdownDisplay: ( + + {indexModeLabels[STANDARD_INDEX_MODE]} + +

{indexModeDescriptions[STANDARD_INDEX_MODE]}

+
+
+ ), + }, + { + value: LOOKUP_INDEX_MODE, + inputDisplay: indexModeLabels[LOOKUP_INDEX_MODE], + 'data-test-subj': 'indexModeLookupOption', + dropdownDisplay: ( + + {indexModeLabels[LOOKUP_INDEX_MODE]} + +

{indexModeDescriptions[LOOKUP_INDEX_MODE]}

+
+
+ ), + }, + ]} + /> +
+ + + +
- - - - - - + + + + + + + setShowApi((prev) => !prev)} + data-test-subj="createIndexShowApiButton" + data-telemetry-id="idxMgmt-indexList-createIndex-showApiButton" + > + {showApi ? ( + + ) : ( + + )} + + + + + + + {showApi && ( + + {apiCode} + + )} + ); diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_modal_v2.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_modal_v2.tsx deleted file mode 100644 index 9ec590f0daedd..0000000000000 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/create_index/create_index_modal_v2.tsx +++ /dev/null @@ -1,300 +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 React, { Fragment, useCallback, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { css } from '@emotion/react'; -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiFieldText, - EuiSuperSelect, - EuiForm, - EuiFormRow, - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, - EuiModalFooter, - EuiSpacer, - EuiText, - useGeneratedHtmlId, - EuiFlexGroup, - EuiFlexItem, - useEuiTheme, - EuiHorizontalRule, - EuiCodeBlock, - type UseEuiTheme, -} from '@elastic/eui'; - -import { LOOKUP_INDEX_MODE, STANDARD_INDEX_MODE } from '../../../../../../common/constants'; -import { indexModeDescriptions, indexModeLabels } from '../../../../lib/index_mode_labels'; -import { createIndex } from '../../../../services'; -import { notificationService } from '../../../../services/notification'; - -import { generateRandomIndexName, isValidIndexName } from './utils'; - -const INVALID_INDEX_NAME_ERROR = i18n.translate( - 'xpack.idxMgmt.createIndex.modal.invalidName.error', - { defaultMessage: 'Index name is not valid' } -); - -const modalHeaderStyles = ({ euiTheme }: UseEuiTheme) => css` - flex-direction: column; - align-items: flex-start; - gap: ${euiTheme.size.s}; -`; - -export interface CreateIndexModalProps { - closeModal: () => void; - loadIndices: () => void; -} - -export const CreateIndexModalV2 = ({ closeModal, loadIndices }: CreateIndexModalProps) => { - const modalTitleId = useGeneratedHtmlId(); - const { euiTheme } = useEuiTheme(); - - const [indexName, setIndexName] = useState(generateRandomIndexName); - - const [indexMode, setIndexMode] = useState(STANDARD_INDEX_MODE); - const [indexNameError, setIndexNameError] = useState(); - const [isSaving, setIsSaving] = useState(false); - const [createError, setCreateError] = useState(); - const [showApi, setShowApi] = useState(false); - - const apiCode = `PUT ${indexName} -{ - "settings": { - "index":{ - "mode":"${indexMode}" - } - } -}`; - - const putCreateIndex = useCallback(async () => { - setIsSaving(true); - try { - const { error } = await createIndex(indexName, indexMode); - setIsSaving(false); - if (!error) { - notificationService.showSuccessToast( - i18n.translate('xpack.idxMgmt.createIndex.successfullyCreatedIndexMessage', { - defaultMessage: 'Successfully created index: {indexName}', - values: { indexName }, - }) - ); - closeModal(); - loadIndices(); - return; - } - setCreateError(error.message); - } catch (e) { - setIsSaving(false); - setCreateError(e.message); - } - }, [closeModal, indexMode, indexName, loadIndices]); - - const onSave = () => { - if (isValidIndexName(indexName)) { - putCreateIndex(); - } - }; - - const onNameChange = (name: string) => { - setIndexName(name); - if (!isValidIndexName(name)) { - setIndexNameError(INVALID_INDEX_NAME_ERROR); - } else if (indexNameError) { - setIndexNameError(undefined); - } - }; - - return ( - - - - - - - - - - - {createError && ( - <> - - - - - - - - )} - - - - - onNameChange(e.target.value)} - data-test-subj="createIndexNameFieldText" - /> - - - - - setIndexMode(mode)} - data-test-subj="indexModeField" - options={[ - { - value: STANDARD_INDEX_MODE, - inputDisplay: indexModeLabels[STANDARD_INDEX_MODE], - 'data-test-subj': 'indexModeStandardOption', - dropdownDisplay: ( - - {indexModeLabels[STANDARD_INDEX_MODE]} - -

{indexModeDescriptions[STANDARD_INDEX_MODE]}

-
-
- ), - }, - { - value: LOOKUP_INDEX_MODE, - inputDisplay: indexModeLabels[LOOKUP_INDEX_MODE], - 'data-test-subj': 'indexModeLookupOption', - dropdownDisplay: ( - - {indexModeLabels[LOOKUP_INDEX_MODE]} - -

{indexModeDescriptions[LOOKUP_INDEX_MODE]}

-
-
- ), - }, - ]} - /> -
-
-
- - -
-
- - - - - - - - setShowApi((prev) => !prev)} - data-test-subj="createIndexShowApiButton" - data-telemetry-id="idxMgmt-indexList-createIndex-showApiButton" - > - {showApi ? ( - - ) : ( - - )} - - - - - - - {showApi && ( - - {apiCode} - - )} - - -
- ); -}; diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page.tsx index df8229acf6dc1..fe056280c88aa 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page.tsx +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page.tsx @@ -18,15 +18,12 @@ import { IndexDetailsSection, Section } from '../../../../../../common/constants import type { Index } from '../../../../../../common'; import type { Error } from '../../../../../shared_imports'; import { loadIndex } from '../../../../services'; -import { useIsPlatformIndexManagementV2Enabled } from '../../../../hooks/use_is_platform_index_management_v2_enabled'; import { DetailsPageError } from './details_page_error'; import { DetailsPageContent } from './details_page_content'; -import { DetailsPageContentV2 } from './details_page_content_v2'; export const DetailsPage: FunctionComponent< RouteComponentProps<{ indexName: string; indexDetailsSection: IndexDetailsSection }> > = ({ location: { search }, history }) => { - const isPlatformIndexManagementV2Enabled = useIsPlatformIndexManagementV2Enabled(); const queryParams = useMemo(() => new URLSearchParams(search), [search]); const indexName = queryParams.get('indexName') ?? ''; const tab: IndexDetailsTabId = queryParams.get('tab') ?? IndexDetailsSection.Overview; @@ -106,18 +103,6 @@ export const DetailsPage: FunctionComponent< /> ); } - if (isPlatformIndexManagementV2Enabled) { - return ( - - ); - } return ( , - order: 10, - }, + // Overview tab is injected in component to pass live docs sample data. { id: IndexDetailsSection.Mappings, name: i18n.translate('xpack.idxMgmt.indexDetails.mappingsTitle', { @@ -91,14 +86,37 @@ export const DetailsPageContent: FunctionComponent = ({ }, config: { enableIndexStats }, plugins: { console: consolePlugin, ml }, - services: { extensionsService }, + services: { extensionsService, notificationService }, } = useAppContext(); const hasMLPermissions = capabilities?.ml?.canGetTrainedModels ? true : false; const indexErrors = useIndexErrors(index, ml, hasMLPermissions); + const { + data: documentsSampleData, + isLoading: isDocumentsSampleLoading, + error: documentsSampleError, + resendRequest: resendDocumentsSampleRequest, + } = useLoadIndexDocumentsSample(index.name); const tabs = useMemo(() => { - const sortedTabs = [...defaultTabs]; + const sortedTabs: IndexDetailsTab[] = [ + { + id: IndexDetailsSection.Overview, + name: i18n.translate('xpack.idxMgmt.indexDetails.overviewTitle', { + defaultMessage: 'Overview', + }), + renderTabContent: ({ index: selectedIndex }) => ( + + ), + order: 10, + }, + ...defaultTabs, + ]; if (enableIndexStats) { sortedTabs.push(statsTab); } @@ -112,7 +130,14 @@ export const DetailsPageContent: FunctionComponent = ({ return tabA.order - tabB.order; }); return sortedTabs; - }, [enableIndexStats, extensionsService.indexDetailsTabs, index]); + }, [ + documentsSampleData, + isDocumentsSampleLoading, + documentsSampleError, + enableIndexStats, + extensionsService.indexDetailsTabs, + index, + ]); const onSectionChange = useCallback( (newSection: IndexDetailsTabId) => { @@ -121,6 +146,10 @@ export const DetailsPageContent: FunctionComponent = ({ [history, index.name, search] ); + const onIndexRefresh = useCallback(() => { + return resendDocumentsSampleRequest(); + }, [resendDocumentsSampleRequest]); + const headerTabs = useMemo(() => { return tabs.map((tabConfig) => ({ onClick: () => onSectionChange(tabConfig.id), @@ -140,37 +169,59 @@ export const DetailsPageContent: FunctionComponent = ({ return ( <> - - - - - - + + + + ), + color: 'primary', + 'aria-current': false, + onClick: (e) => { + e.preventDefault(); + navigateToIndicesList(); + }, + }, + ]} bottomBorder + tabs={headerTabs} rightSideItems={[ - , , + , + + openWiredConnectionDetails({ + props: { options: { defaultTabId: 'apiKeys' } }, + }).catch((error) => { + notificationService.showDangerToast( + error?.body?.message ?? error?.message ?? 'An unexpected error occurred' + ); + }) + } + iconType="plugs" + data-test-subj="openConnectionDetails" + > + + , ]} - rightSideGroupProps={{ - wrap: false, - }} - responsive="reverse" - tabs={headerTabs} > {indexErrors.length > 0 ? : null} diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_content_v2.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_content_v2.tsx deleted file mode 100644 index 531ccf294e0df..0000000000000 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_content_v2.tsx +++ /dev/null @@ -1,218 +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 type { FunctionComponent } from 'react'; -import React, { useCallback, useMemo } from 'react'; -import type { EuiPageHeaderProps } from '@elastic/eui'; -import { EuiButtonEmpty, EuiPageHeader, EuiSpacer } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import type { RouteComponentProps } from 'react-router-dom'; -import { openWiredConnectionDetails } from '@kbn/cloud/connection_details'; - -import { useIndexErrors } from '../../../../hooks/use_index_errors'; -import { resetIndexUrlParams } from './reset_index_url_params'; -import { renderBadges } from '../../../../lib/render_badges'; -import { useLoadIndexDocumentsSample } from '../../../../services/api'; -import type { Index } from '../../../../../../common'; -import type { IndexDetailsTab, IndexDetailsTabId } from '../../../../../../common/constants'; -import { INDEX_OPEN, IndexDetailsSection } from '../../../../../../common/constants'; -import { getIndexDetailsLink } from '../../../../services/routing'; -import { useAppContext } from '../../../../app_context'; -import { DiscoverLink } from '../../../../lib/discover_link'; -import { ManageIndexButton } from './manage_index_button'; -import { DetailsPageMappings } from './details_page_mappings'; -import { DetailsPageSettings } from './details_page_settings'; -import { DetailsPageStats } from './details_page_stats'; -import { DetailsPageTab } from './details_page_tab'; -import { IndexErrorCallout } from './index_error_callout'; -import { DetailsPageOverviewV2 } from './details_page_overview/details_page_overview_v2'; - -const defaultTabs: IndexDetailsTab[] = [ - // Overview tab is injected in component to pass live docs sample data. - { - id: IndexDetailsSection.Mappings, - name: i18n.translate('xpack.idxMgmt.indexDetails.mappingsTitle', { - defaultMessage: 'Mappings', - }), - renderTabContent: ({ index }) => , - order: 20, - }, - { - id: IndexDetailsSection.Settings, - name: i18n.translate('xpack.idxMgmt.indexDetails.settingsTitle', { - defaultMessage: 'Settings', - }), - renderTabContent: ({ index }) => , - order: 30, - }, -]; - -const statsTab: IndexDetailsTab = { - id: IndexDetailsSection.Stats, - name: i18n.translate('xpack.idxMgmt.indexDetails.statsTitle', { - defaultMessage: 'Statistics', - }), - renderTabContent: ({ index }) => ( - - ), - order: 40, -}; - -interface Props { - index: Index; - tab: IndexDetailsTabId; - history: RouteComponentProps['history']; - search: string; - fetchIndexDetails: () => Promise; - navigateToIndicesList: () => void; -} -export const DetailsPageContentV2: FunctionComponent = ({ - index, - tab, - history, - search, - fetchIndexDetails, - navigateToIndicesList, -}) => { - const { - core: { - application: { capabilities }, - }, - config: { enableIndexStats }, - plugins: { console: consolePlugin, ml }, - services: { extensionsService, notificationService }, - } = useAppContext(); - const hasMLPermissions = capabilities?.ml?.canGetTrainedModels ? true : false; - - const indexErrors = useIndexErrors(index, ml, hasMLPermissions); - const { - data: documentsSampleData, - isLoading: isDocumentsSampleLoading, - error: documentsSampleError, - resendRequest: resendDocumentsSampleRequest, - } = useLoadIndexDocumentsSample(index.name); - - const tabs = useMemo(() => { - const sortedTabs: IndexDetailsTab[] = [ - { - id: IndexDetailsSection.Overview, - name: i18n.translate('xpack.idxMgmt.indexDetails.overviewTitle', { - defaultMessage: 'Overview', - }), - renderTabContent: ({ index: selectedIndex }) => ( - - ), - order: 10, - }, - ...defaultTabs, - ]; - if (enableIndexStats) { - sortedTabs.push(statsTab); - } - extensionsService.indexDetailsTabs.forEach((dynamicTab) => { - if (!dynamicTab.shouldRenderTab || dynamicTab.shouldRenderTab({ index })) { - sortedTabs.push(dynamicTab); - } - }); - - sortedTabs.sort((tabA, tabB) => { - return tabA.order - tabB.order; - }); - return sortedTabs; - }, [ - documentsSampleData, - isDocumentsSampleLoading, - documentsSampleError, - enableIndexStats, - extensionsService.indexDetailsTabs, - index, - ]); - - const onSectionChange = useCallback( - (newSection: IndexDetailsTabId) => { - return history.push(getIndexDetailsLink(index.name, resetIndexUrlParams(search), newSection)); - }, - [history, index.name, search] - ); - - const onIndexRefresh = useCallback(() => { - return resendDocumentsSampleRequest(); - }, [resendDocumentsSampleRequest]); - - const headerTabs = useMemo(() => { - return tabs.map((tabConfig) => ({ - onClick: () => onSectionChange(tabConfig.id), - isSelected: tabConfig.id === tab, - key: tabConfig.id, - 'data-test-subj': `indexDetailsTab-${tabConfig.id}`, - label: tabConfig.name, - })); - }, [tabs, tab, onSectionChange]); - - const pageTitle = ( - <> - {index.name} - {renderBadges(index, extensionsService)} - - ); - - return ( - <> - , - , - - openWiredConnectionDetails({ - props: { options: { defaultTabId: 'apiKeys' } }, - }).catch((error) => { - notificationService.showDangerToast(error.body.message); - }) - } - iconType="plugs" - data-test-subj="openConnectionDetails" - > - - , - ]} - > - {indexErrors.length > 0 ? : null} - - -
- -
- {consolePlugin?.EmbeddableConsole ? : null} - - ); -}; diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.test.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.test.tsx new file mode 100644 index 0000000000000..ecab7ad152c96 --- /dev/null +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.test.tsx @@ -0,0 +1,232 @@ +/* + * 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 { render, screen, waitFor } from '@testing-library/react'; +import type { SearchHit } from '@elastic/elasticsearch/lib/api/types'; + +import type { Index } from '../../../../../../../common'; +import { DetailsPageOverview } from './details_page_overview'; +import { + setupEnvironment, + WithAppDependencies, +} from '../../../../../../../__jest__/client_integration/helpers/setup_environment'; +import { + testIndexMock, + testIndexName, + testIndexMappings, + testUserStartPrivilegesResponse, +} from '../../../../../../../__jest__/client_integration/index_details_page/mocks'; + +jest.mock('@kbn/code-editor'); + +const mockUseCloudConnectStatus = jest.fn(); +jest.mock('@kbn/search-api-panels', () => ({ + ...jest.requireActual('@kbn/search-api-panels'), + useCloudConnectStatus: (...args: unknown[]) => mockUseCloudConnectStatus(...args), + EisCloudConnectPromoCallout: (props: { promoId: string }) => ( +
Cloud Connect Promo
+ ), + EisUpdateCallout: (props: { promoId: string; handleOnClick: () => void }) => ( +
+ EIS Update Callout + +
+ ), +})); + +const mockHasElserOnMlNodeSemanticTextField = jest.fn(); +jest.mock('../../../../../components/mappings_editor/lib/utils', () => ({ + ...jest.requireActual('../../../../../components/mappings_editor/lib/utils'), + hasElserOnMlNodeSemanticTextField: (...args: unknown[]) => + mockHasElserOnMlNodeSemanticTextField(...args), +})); + +jest.mock('../update_elser_mappings/update_elser_mappings_modal', () => ({ + UpdateElserMappingsModal: (props: { indexName: string }) => ( +
Update ELSER Mappings for {props.indexName}
+ ), +})); + +describe('DetailsPageOverview', () => { + let httpSetup: ReturnType['httpSetup']; + let httpRequestsMockHelpers: ReturnType['httpRequestsMockHelpers']; + + beforeEach(() => { + jest.clearAllMocks(); + const mockEnvironment = setupEnvironment(); + ({ httpSetup, httpRequestsMockHelpers } = mockEnvironment); + + httpRequestsMockHelpers.setLoadIndexMappingResponse(testIndexName, testIndexMappings); + httpRequestsMockHelpers.setUserStartPrivilegesResponse( + testIndexName, + testUserStartPrivilegesResponse + ); + httpRequestsMockHelpers.setLoadIndexDocCountResponse({ [testIndexName]: 1 }); + httpRequestsMockHelpers.setInferenceModels([]); + + mockUseCloudConnectStatus.mockReturnValue({ + isLoading: true, + isCloudConnected: false, + isCloudConnectedWithEisEnabled: false, + }); + mockHasElserOnMlNodeSemanticTextField.mockReturnValue(false); + }); + + const renderComponent = ( + overrides: { + indexDetails?: Index; + sampleDocuments?: SearchHit[]; + isDocumentsLoading?: boolean; + documentsError?: unknown; + appDeps?: Record; + } = {} + ) => { + const defaultProps = { + indexDetails: overrides.indexDetails ?? testIndexMock, + sampleDocuments: overrides.sampleDocuments ?? [], + isDocumentsLoading: overrides.isDocumentsLoading ?? false, + documentsError: overrides.documentsError ?? undefined, + }; + + const Comp = WithAppDependencies(() => , httpSetup, { + url: { locators: { get: () => ({ navigate: jest.fn(), getUrl: jest.fn() }) } }, + ...overrides.appDeps, + }); + + return render(); + }; + + it('renders the QuickStats section', async () => { + renderComponent(); + await waitFor(() => { + expect(screen.getByText('Storage')).toBeInTheDocument(); + }); + }); + + it('renders the "Add data to this index" heading', async () => { + renderComponent(); + await waitFor(() => { + expect(screen.getByText('Add data to this index')).toBeInTheDocument(); + }); + }); + + it('renders the bulk API description with a docs link', async () => { + renderComponent(); + await waitFor(() => { + expect(screen.getByText('Learn more.')).toBeInTheDocument(); + }); + }); + + it('renders code snippet with the index name', async () => { + renderComponent(); + await waitFor(() => { + expect(screen.getByText(/test_index/)).toBeInTheDocument(); + }); + }); + + describe('EisCloudConnectPromoCallout', () => { + it('does not render when cloud connect status is loading', async () => { + mockUseCloudConnectStatus.mockReturnValue({ + isLoading: true, + isCloudConnected: false, + isCloudConnectedWithEisEnabled: false, + }); + + renderComponent(); + await waitFor(() => { + expect(screen.getByText('Add data to this index')).toBeInTheDocument(); + }); + expect( + screen.queryByTestId('indexDetailsOverview-cloud-connect-callout') + ).not.toBeInTheDocument(); + }); + + it('renders when not loading and not cloud connected', async () => { + mockUseCloudConnectStatus.mockReturnValue({ + isLoading: false, + isCloudConnected: false, + isCloudConnectedWithEisEnabled: false, + }); + + renderComponent(); + await waitFor(() => { + expect( + screen.getByTestId('indexDetailsOverview-cloud-connect-callout') + ).toBeInTheDocument(); + }); + }); + + it('does not render when already cloud connected', async () => { + mockUseCloudConnectStatus.mockReturnValue({ + isLoading: false, + isCloudConnected: true, + isCloudConnectedWithEisEnabled: false, + }); + + renderComponent(); + await waitFor(() => { + expect(screen.getByText('Add data to this index')).toBeInTheDocument(); + }); + expect( + screen.queryByTestId('indexDetailsOverview-cloud-connect-callout') + ).not.toBeInTheDocument(); + }); + }); + + describe('EisUpdateCallout', () => { + it('does not render when there are no ELSER semantic text fields', async () => { + mockHasElserOnMlNodeSemanticTextField.mockReturnValue(false); + + renderComponent(); + await waitFor(() => { + expect(screen.getByText('Add data to this index')).toBeInTheDocument(); + }); + expect( + screen.queryByTestId('indexDetailsOverview-eis-update-callout') + ).not.toBeInTheDocument(); + }); + + it('renders when ELSER semantic text fields are present', async () => { + mockHasElserOnMlNodeSemanticTextField.mockReturnValue(true); + + renderComponent(); + await waitFor(() => { + expect(screen.getByTestId('indexDetailsOverview-eis-update-callout')).toBeInTheDocument(); + }); + }); + }); + + describe('UpdateElserMappingsModal', () => { + it('does not render by default', async () => { + mockHasElserOnMlNodeSemanticTextField.mockReturnValue(true); + + renderComponent(); + await waitFor(() => { + expect(screen.getByTestId('indexDetailsOverview-eis-update-callout')).toBeInTheDocument(); + }); + expect(screen.queryByTestId('updateElserMappingsModal')).not.toBeInTheDocument(); + }); + + it('renders after clicking the EIS update callout CTA button', async () => { + mockHasElserOnMlNodeSemanticTextField.mockReturnValue(true); + + renderComponent(); + await waitFor(() => { + expect(screen.getByTestId('eisUpdateCalloutCtaBtn')).toBeInTheDocument(); + }); + + screen.getByTestId('eisUpdateCalloutCtaBtn').click(); + + await waitFor(() => { + expect(screen.getByTestId('updateElserMappingsModal')).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.tsx index 5c020b61a6062..d01948578b8d2 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.tsx +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { @@ -16,8 +16,6 @@ import { EuiText, EuiTextColor, EuiLink, - EuiFlexGrid, - useIsWithinBreakpoints, } from '@elastic/eui'; import type { LanguageDefinition, @@ -32,18 +30,11 @@ import { useCloudConnectStatus, } from '@kbn/search-api-panels'; import { CLOUD_CONNECT_NAV_ID } from '@kbn/deeplinks-management/constants'; +import type { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { type Index } from '../../../../../../../common'; -import { formatBytes } from '../../../../../lib/format_bytes'; import { useAppContext } from '../../../../../app_context'; import { documentationService, useLoadIndexMappings } from '../../../../../services'; -import { loadIndexDocCount } from '../../../../../services/api'; import { languageDefinitions, curlDefinition } from './languages'; -import type { DocCountState } from '../quick_stats/quick_stats'; -import { StatusDetails } from '../quick_stats/status_details'; -import { DataStreamDetails } from '../quick_stats/data_stream_details'; -import { StorageDetails } from '../quick_stats/storage_details'; -import { AliasesDetails } from '../quick_stats/aliases_details'; -import { SizeDocCountDetails } from '../quick_stats/size_doc_count_details'; import { UpdateElserMappingsModal } from '../update_elser_mappings/update_elser_mappings_modal'; import { useMappingsState } from '../../../../../components/mappings_editor/mappings_state_context'; @@ -52,24 +43,23 @@ import { useMappingsStateListener } from '../../../../../components/mappings_edi import { parseMappings } from '../../../../../shared/parse_mappings'; import { useUserPrivileges } from '../../../../../services/api'; import { useLicense } from '../../../../../../hooks/use_license'; +import { IndexDocuments } from '../index_documents/index_documents'; +import { QuickStats } from '../quick_stats/quick_stats'; interface Props { indexDetails: Index; + sampleDocuments: SearchHit[]; + isDocumentsLoading: boolean; + documentsError: unknown; } -export const DetailsPageOverview: React.FunctionComponent = ({ indexDetails }) => { - const { - name, - status, - health, - documents_deleted: documentsDeleted, - primary, - replica, - aliases, - data_stream: dataStream, - size, - primary_size: primarySize, - } = indexDetails; +export const DetailsPageOverview: React.FunctionComponent = ({ + indexDetails, + sampleDocuments, + isDocumentsLoading, + documentsError, +}) => { + const { name } = indexDetails; const { core, plugins: { cloud, cloudConnect, share }, @@ -79,25 +69,6 @@ export const DetailsPageOverview: React.FunctionComponent = ({ indexDetai const { data: mappingsData, resendRequest } = useLoadIndexMappings(name || ''); const { isAtLeastEnterprise } = useLicense(); - const [docCount, setDocCount] = useState({ isLoading: true, isError: false }); - - const fetchDocCount = useCallback(async () => { - try { - const { data, error } = await loadIndexDocCount(name); - if (error || !data) { - setDocCount({ isLoading: false, isError: true }); - } else { - setDocCount({ count: data[name], isLoading: false, isError: false }); - } - } catch { - setDocCount({ isLoading: false, isError: true }); - } - }, [name]); - - useEffect(() => { - fetchDocCount(); - }, [fetchDocCount]); - const [selectedLanguage, setSelectedLanguage] = useState(curlDefinition); const [elasticsearchUrl, setElasticsearchUrl] = useState(''); const hasElserOnMlNodeSemanticText = hasElserOnMlNodeSemanticTextField(state.mappingViewFields); @@ -106,16 +77,12 @@ export const DetailsPageOverview: React.FunctionComponent = ({ indexDetai const { data } = useUserPrivileges(indexDetails.name); const hasUpdateMappingsPrivileges = data?.privileges?.canManageIndex === true; - const sizeFormatted = formatBytes(size); - const primarySizeFormatted = formatBytes(primarySize); - const codeSnippetArguments: LanguageDefinitionSnippetArguments = { url: elasticsearchUrl, apiKey: 'your_api_key', indexName: name, }; - const isLarge = useIsWithinBreakpoints(['xl']); const { isLoading: isCloudConnectStatusLoading, isCloudConnected, @@ -174,27 +141,7 @@ export const DetailsPageOverview: React.FunctionComponent = ({ indexDetai /> )} - - - - - - - - - - {dataStream && } - + @@ -254,6 +201,12 @@ export const DetailsPageOverview: React.FunctionComponent = ({ indexDetai consoleRequest={getConsoleRequest('ingestDataIndex', codeSnippetArguments)} /> + )} diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview_v2.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview_v2.tsx deleted file mode 100644 index 5548d56760fb6..0000000000000 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview_v2.tsx +++ /dev/null @@ -1,214 +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 React, { useState, useEffect, useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiText, - EuiTextColor, - EuiLink, -} from '@elastic/eui'; -import type { - LanguageDefinition, - LanguageDefinitionSnippetArguments, -} from '@kbn/search-api-panels'; -import { - CodeBox, - getLanguageDefinitionCodeSnippet, - getConsoleRequest, - EisCloudConnectPromoCallout, - EisUpdateCallout, - useCloudConnectStatus, -} from '@kbn/search-api-panels'; -import { CLOUD_CONNECT_NAV_ID } from '@kbn/deeplinks-management/constants'; -import type { SearchHit } from '@elastic/elasticsearch/lib/api/types'; -import { type Index } from '../../../../../../../common'; -import { useAppContext } from '../../../../../app_context'; -import { documentationService, useLoadIndexMappings } from '../../../../../services'; -import { languageDefinitions, curlDefinition } from './languages'; - -import { UpdateElserMappingsModal } from '../update_elser_mappings/update_elser_mappings_modal'; -import { useMappingsState } from '../../../../../components/mappings_editor/mappings_state_context'; -import { hasElserOnMlNodeSemanticTextField } from '../../../../../components/mappings_editor/lib/utils'; -import { useMappingsStateListener } from '../../../../../components/mappings_editor/use_state_listener'; -import { parseMappings } from '../../../../../shared/parse_mappings'; -import { useUserPrivileges } from '../../../../../services/api'; -import { useLicense } from '../../../../../../hooks/use_license'; -import { IndexDocuments } from '../index_documents/index_documents'; -import { QuickStats } from '../quick_stats/quick_stats'; - -interface Props { - indexDetails: Index; - sampleDocuments: SearchHit[]; - isDocumentsLoading: boolean; - documentsError: unknown; -} - -export const DetailsPageOverviewV2: React.FunctionComponent = ({ - indexDetails, - sampleDocuments, - isDocumentsLoading, - documentsError, -}) => { - const { name } = indexDetails; - const { - core, - plugins: { cloud, cloudConnect, share }, - services: { extensionsService }, - } = useAppContext(); - const state = useMappingsState(); - const { data: mappingsData, resendRequest } = useLoadIndexMappings(name || ''); - const { isAtLeastEnterprise } = useLicense(); - - const [selectedLanguage, setSelectedLanguage] = useState(curlDefinition); - const [elasticsearchUrl, setElasticsearchUrl] = useState(''); - const hasElserOnMlNodeSemanticText = hasElserOnMlNodeSemanticTextField(state.mappingViewFields); - const [isUpdatingElserMappings, setIsUpdatingElserMappings] = useState(false); - - const { data } = useUserPrivileges(indexDetails.name); - const hasUpdateMappingsPrivileges = data?.privileges?.canManageIndex === true; - - const codeSnippetArguments: LanguageDefinitionSnippetArguments = { - url: elasticsearchUrl, - apiKey: 'your_api_key', - indexName: name, - }; - - const { - isLoading: isCloudConnectStatusLoading, - isCloudConnected, - isCloudConnectedWithEisEnabled, - } = useCloudConnectStatus(cloudConnect?.hooks.useCloudConnectStatus); - - const shouldShowEisUpdateCallout = - ((cloud?.isCloudEnabled || isCloudConnectedWithEisEnabled) && - (isAtLeastEnterprise() || cloud?.isServerlessEnabled)) ?? - false; - - const { parsedDefaultValue } = useMemo( - () => parseMappings(mappingsData ?? undefined), - [mappingsData] - ); - - useMappingsStateListener({ value: parsedDefaultValue, status: 'disabled' }); - - useEffect(() => { - cloud?.fetchElasticsearchConfig().then((config) => { - setElasticsearchUrl(config.elasticsearchUrl || 'https://your_deployment_url'); - }); - }, [cloud]); - - return ( - <> - {!isCloudConnectStatusLoading && !isCloudConnected && ( - - core.application.navigateToApp(CLOUD_CONNECT_NAV_ID, { openInNewTab: true }) - } - addSpacer="bottom" - /> - )} - {hasElserOnMlNodeSemanticText && ( - setIsUpdatingElserMappings(true)} - direction="row" - hasUpdatePrivileges={hasUpdateMappingsPrivileges} - addSpacer="bottom" - /> - )} - {isUpdatingElserMappings && ( - - )} - - - - - - {extensionsService.indexOverviewContent ? ( - extensionsService.indexOverviewContent.renderContent({ - index: indexDetails, - getUrlForApp: core.getUrlForApp, - }) - ) : ( - - - -

- {i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.addMoreDataTitle', { - defaultMessage: 'Add data to this index', - })} -

-
- - - - - -

- - - - ), - }} - /> -

-
-
-
- - - - - -
- )} - - ); -}; diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/index_documents/document_list.test.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/index_documents/document_list.test.tsx new file mode 100644 index 0000000000000..f600bbc602f18 --- /dev/null +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/index_documents/document_list.test.tsx @@ -0,0 +1,84 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import type { SearchHit, MappingProperty } from '@elastic/elasticsearch/lib/api/types'; + +import { DocumentList } from './document_list'; + +jest.mock('@kbn/search-index-documents', () => ({ + Result: ({ metaData, compactCard }: { metaData: { id: string }; compactCard: boolean }) => ( +
+ Result {metaData.id} +
+ ), + resultMetaData: jest.fn((doc: SearchHit) => ({ + id: doc._id, + title: undefined, + score: undefined, + })), + resultToField: jest.fn(() => []), + reorderFieldsInImportance: jest.fn((fields: unknown[]) => fields), +})); + +jest.mock('./recent_docs_action_message', () => ({ + RecentDocsActionMessage: ({ numOfDocs }: { numOfDocs: number }) => ( +
{numOfDocs} documents
+ ), +})); + +const { resultMetaData, resultToField } = jest.requireMock('@kbn/search-index-documents'); + +const mockDocs: SearchHit[] = [ + { _index: 'test-index', _id: 'doc-1', _source: { title: 'First' } }, + { _index: 'test-index', _id: 'doc-2', _source: { title: 'Second' } }, + { _index: 'test-index', _id: 'doc-3', _source: { title: 'Third' } }, +]; + +const mockMappingProperties: Record = { + title: { type: 'text' }, + count: { type: 'integer' }, +}; + +describe('DocumentList', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders RecentDocsActionMessage with the correct doc count', () => { + render(); + + expect(screen.getByTestId('recentDocsActionMessage')).toHaveTextContent('3 documents'); + }); + + it('renders a Result component for each document', () => { + render(); + + expect(screen.getByTestId('result-doc-1')).toBeInTheDocument(); + expect(screen.getByTestId('result-doc-2')).toBeInTheDocument(); + expect(screen.getByTestId('result-doc-3')).toBeInTheDocument(); + }); + + it('calls resultMetaData for each document', () => { + render(); + + expect(resultMetaData).toHaveBeenCalledTimes(3); + expect(resultMetaData).toHaveBeenCalledWith(mockDocs[0]); + expect(resultMetaData).toHaveBeenCalledWith(mockDocs[1]); + expect(resultMetaData).toHaveBeenCalledWith(mockDocs[2]); + }); + + it('calls resultToField with each doc and mappingProperties', () => { + render(); + + expect(resultToField).toHaveBeenCalledTimes(3); + expect(resultToField).toHaveBeenCalledWith(mockDocs[0], mockMappingProperties); + expect(resultToField).toHaveBeenCalledWith(mockDocs[1], mockMappingProperties); + expect(resultToField).toHaveBeenCalledWith(mockDocs[2], mockMappingProperties); + }); +});