From d482a0a295cfc55c6635f32741543bde7cb38884 Mon Sep 17 00:00:00 2001 From: Saarika Bhasi <55930906+saarikabhasi@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:21:59 -0400 Subject: [PATCH] [Onboarding][Index detail] Update right side menu items (#194604) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary In this PR, updates search index detail page right side menu item. The drop down menu item is updated to have : * Api reference doc link * Use in playground link which navigates to playground selecting this index name When documents exists right side header action is replaced with `Use in playground` else `Api reference doc link` ### Screenshot Screenshot 2024-10-01 at 11 07 45 AM Screenshot 2024-10-01 at 11 08 20 AM **How to test:** 1. Enable searchIndices plugin in `kibana.dev.yml` as this plugin is behind Feature flag ``` xpack.searchIndices.enabled: true ``` 2. [Create new index](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html) 3. Navigate to `/app/elasticsearch/indices/index_details/${indexName}/data` 4. Confirm index header action is `Api Reference doc` 5. Add documents confirm index header action is changed to `Use in playground` 6. Confirm menu item shows delete index, use in playground & api reference doc link ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --- .../index_documents/index_documents.tsx | 18 ++- .../components/indices/details_page.tsx | 135 ++++++++++-------- .../indices/details_page_menu_item.tsx | 132 +++++++++++++++++ .../public/hooks/api/use_document_search.ts | 2 +- .../svl_search_index_detail_page.ts | 18 ++- .../test_suites/search/search_index_detail.ts | 13 +- 6 files changed, 243 insertions(+), 75 deletions(-) create mode 100644 x-pack/plugins/search_indices/public/components/indices/details_page_menu_item.tsx diff --git a/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx b/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx index 9b8852eb596bc..83595913cece3 100644 --- a/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx +++ b/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx @@ -8,25 +8,23 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiProgress, EuiSpacer } from '@elastic/eui'; -import { useIndexDocumentSearch } from '../../hooks/api/use_document_search'; import { useIndexMapping } from '../../hooks/api/use_index_mappings'; import { AddDocumentsCodeExample } from './add_documents_code_example'; - -import { DEFAULT_PAGE_SIZE } from './constants'; +import { IndexDocuments as IndexDocumentsType } from '../../hooks/api/use_document_search'; import { DocumentList } from './document_list'; interface IndexDocumentsProps { indexName: string; + indexDocuments?: IndexDocumentsType; + isInitialLoading: boolean; } -export const IndexDocuments: React.FC = ({ indexName }) => { - const { data: indexDocuments, isInitialLoading } = useIndexDocumentSearch(indexName, { - pageSize: DEFAULT_PAGE_SIZE, - pageIndex: 0, - }); - +export const IndexDocuments: React.FC = ({ + indexName, + indexDocuments, + isInitialLoading, +}) => { const { data: mappingData } = useIndexMapping(indexName); - const docs = indexDocuments?.results?.data ?? []; const mappingProperties = mappingData?.mappings?.properties ?? {}; diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx index 0a3de4f570551..f12618f4a76bd 100644 --- a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx +++ b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx @@ -11,12 +11,6 @@ import { EuiPageTemplate, EuiFlexItem, EuiFlexGroup, - EuiPopover, - EuiButtonIcon, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiText, - EuiIcon, EuiButtonEmpty, EuiTabbedContent, EuiTabbedContentTab, @@ -38,12 +32,15 @@ import { IndexloadingError } from './details_page_loading_error'; import { SearchIndexDetailsTabs } from '../../routes'; import { SearchIndexDetailsMappings } from './details_page_mappings'; import { SearchIndexDetailsSettings } from './details_page_settings'; +import { SearchIndexDetailsPageMenuItemPopover } from './details_page_menu_item'; +import { useIndexDocumentSearch } from '../../hooks/api/use_document_search'; +import { DEFAULT_PAGE_SIZE } from '../index_documents/constants'; export const SearchIndexDetailsPage = () => { const indexName = decodeURIComponent(useParams<{ indexName: string }>().indexName); const tabId = decodeURIComponent(useParams<{ tabId: string }>().tabId); - const { console: consolePlugin, docLinks, application, history } = useKibana().services; + const { console: consolePlugin, docLinks, application, history, share } = useKibana().services; const { data: index, refetch, @@ -57,6 +54,25 @@ export const SearchIndexDetailsPage = () => { isInitialLoading: isMappingsInitialLoading, error: mappingsError, } = useIndexMapping(indexName); + const { data: indexDocuments, isInitialLoading: indexDocumentsIsInitialLoading } = + useIndexDocumentSearch(indexName, { + pageSize: DEFAULT_PAGE_SIZE, + pageIndex: 0, + }); + + const navigateToPlayground = useCallback(async () => { + const playgroundLocator = share.url.locators.get('PLAYGROUND_LOCATOR_ID'); + if (playgroundLocator && index) { + await playgroundLocator.navigate({ 'default-index': index.name }); + } + }, [share, index]); + + const [isDocumentsExists, setDocumentsExists] = useState(false); + const [isDocumentsLoading, setDocumentsLoading] = useState(true); + useEffect(() => { + setDocumentsLoading(isInitialLoading); + setDocumentsExists(!(!isInitialLoading && indexDocuments?.results?.data.length === 0)); + }, [indexDocuments, isInitialLoading, setDocumentsExists, setDocumentsLoading]); const detailsPageTabs: EuiTabbedContentTab[] = useMemo(() => { return [ @@ -65,7 +81,13 @@ export const SearchIndexDetailsPage = () => { name: i18n.translate('xpack.searchIndices.documentsTabLabel', { defaultMessage: 'Data', }), - content: , + content: ( + + ), 'data-test-subj': `${SearchIndexDetailsTabs.DATA}Tab`, }, { @@ -85,7 +107,7 @@ export const SearchIndexDetailsPage = () => { 'data-test-subj': `${SearchIndexDetailsTabs.SETTINGS}Tab`, }, ]; - }, [index, indexName]); + }, [index, indexName, indexDocuments, indexDocumentsIsInitialLoading]); const [selectedTab, setSelectedTab] = useState(detailsPageTabs[0]); useEffect(() => { @@ -124,47 +146,11 @@ export const SearchIndexDetailsPage = () => { }, [isIndexError, indexLoadingError, mappingsError] ); - const [showMoreOptions, setShowMoreOptions] = useState(false); const [isShowingDeleteModal, setShowDeleteIndexModal] = useState(false); - const moreOptionsPopover = ( - setShowMoreOptions(!showMoreOptions)} - button={ - setShowMoreOptions(!showMoreOptions)} - size="m" - data-test-subj="moreOptionsActionButton" - aria-label={i18n.translate('xpack.searchIndices.moreOptions.ariaLabel', { - defaultMessage: 'More options', - })} - /> - } - > - } - onClick={() => { - setShowDeleteIndexModal(!isShowingDeleteModal); - }} - size="s" - color="danger" - data-test-subj="moreOptionsDeleteIndex" - > - - {i18n.translate('xpack.searchIndices.moreOptions.deleteIndexLabel', { - defaultMessage: 'Delete Index', - })} - - , - ]} - /> - - ); + const handleDeleteIndexModal = useCallback(() => { + setShowDeleteIndexModal(!isShowingDeleteModal); + }, [isShowingDeleteModal]); + if (isInitialLoading || isMappingsInitialLoading) { return ( @@ -209,20 +195,47 @@ export const SearchIndexDetailsPage = () => { data-test-subj="searchIndexDetailsHeader" pageTitle={index?.name} rightSideItems={[ - + - - {i18n.translate('xpack.searchIndices.indexActionsMenu.apiReference.docLink', { - defaultMessage: 'API Reference', - })} - + {!isDocumentsExists ? ( + + + + ) : ( + + + + )} + + + - {moreOptionsPopover} , ]} /> diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page_menu_item.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page_menu_item.tsx new file mode 100644 index 0000000000000..77c2d9c6a8ee1 --- /dev/null +++ b/x-pack/plugins/search_indices/public/components/indices/details_page_menu_item.tsx @@ -0,0 +1,132 @@ +/* + * 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 { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiIcon, + EuiPopover, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { MouseEventHandler, ReactElement, useState } from 'react'; +import { useKibana } from '../../hooks/use_kibana'; + +enum MenuItems { + playground = 'playground', + apiReference = 'apiReference', + deleteIndex = 'deleteIndex', +} +interface MenuItemsAction { + href?: string; + onClick?: (() => void) | MouseEventHandler; +} + +const SearchIndexDetailsPageMenuItemPopoverItems = [ + { + type: MenuItems.playground, + iconType: 'launch', + dataTestSubj: 'moreOptionsPlayground', + iconComponent: , + target: undefined, + text: ( + + {i18n.translate('xpack.searchIndices.moreOptions.playgroundLabel', { + defaultMessage: 'Use in Playground', + })} + + ), + color: undefined, + }, + { + type: MenuItems.apiReference, + iconType: 'documentation', + dataTestSubj: 'moreOptionsApiReference', + iconComponent: , + target: '_blank', + text: ( + + {i18n.translate('xpack.searchIndices.moreOptions.apiReferenceLabel', { + defaultMessage: 'API Reference', + })} + + ), + color: undefined, + }, + { + type: MenuItems.deleteIndex, + iconType: 'trash', + dataTestSubj: 'moreOptionsDeleteIndex', + iconComponent: , + target: undefined, + text: ( + + {i18n.translate('xpack.searchIndices.moreOptions.deleteIndexLabel', { + defaultMessage: 'Delete Index', + })} + + ), + color: 'danger', + }, +]; +interface SearchIndexDetailsPageMenuItemPopoverProps { + handleDeleteIndexModal: () => void; + navigateToPlayground: () => void; +} + +export const SearchIndexDetailsPageMenuItemPopover = ({ + handleDeleteIndexModal, + navigateToPlayground, +}: SearchIndexDetailsPageMenuItemPopoverProps) => { + const [showMoreOptions, setShowMoreOptions] = useState(false); + const { docLinks } = useKibana().services; + const contextMenuItemsActions: Record = { + playground: { + href: undefined, + onClick: navigateToPlayground, + }, + apiReference: { href: docLinks.links.apiReference, onClick: undefined }, + deleteIndex: { href: undefined, onClick: handleDeleteIndexModal }, + }; + const contextMenuItems: ReactElement[] = SearchIndexDetailsPageMenuItemPopoverItems.map( + (item) => ( + + {item.text} + + ) + ); + + return ( + setShowMoreOptions(!showMoreOptions)} + button={ + setShowMoreOptions(!showMoreOptions)} + size="m" + data-test-subj="moreOptionsActionButton" + aria-label={i18n.translate('xpack.searchIndices.moreOptions.ariaLabel', { + defaultMessage: 'More options', + })} + /> + } + > + + + ); +}; diff --git a/x-pack/plugins/search_indices/public/hooks/api/use_document_search.ts b/x-pack/plugins/search_indices/public/hooks/api/use_document_search.ts index 8a0570844b1f9..c1ee54088bfe3 100644 --- a/x-pack/plugins/search_indices/public/hooks/api/use_document_search.ts +++ b/x-pack/plugins/search_indices/public/hooks/api/use_document_search.ts @@ -11,7 +11,7 @@ import { pageToPagination, Paginate } from '@kbn/search-index-documents'; import { useQuery } from '@tanstack/react-query'; import { useKibana } from '../use_kibana'; -interface IndexDocuments { +export interface IndexDocuments { meta: Pagination; results: Paginate; } diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts index dabc8dffac09e..d3d459f6225dd 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts @@ -20,6 +20,9 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont async expectAPIReferenceDocLinkExists() { await testSubjects.existOrFail('ApiReferenceDoc', { timeout: 2000 }); }, + async expectUseInPlaygroundLinkExists() { + await testSubjects.existOrFail('useInPlaygroundLink', { timeout: 5000 }); + }, async expectBackToIndicesButtonExists() { await testSubjects.existOrFail('backToIndicesButton', { timeout: 2000 }); }, @@ -82,7 +85,20 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont async expectMoreOptionsOverviewMenuIsShown() { await testSubjects.existOrFail('moreOptionsContextMenu'); }, - async expectDeleteIndexButtonExists() { + async expectPlaygroundButtonExistsInMoreOptions() { + await testSubjects.existOrFail('moreOptionsPlayground'); + }, + async expectToNavigateToPlayground(indexName: string) { + await testSubjects.click('moreOptionsPlayground'); + expect(await browser.getCurrentUrl()).contain( + `/search_playground/chat?default-index=${indexName}` + ); + await testSubjects.existOrFail('chatPage'); + }, + async expectAPIReferenceDocLinkExistsInMoreOptions() { + await testSubjects.existOrFail('moreOptionsApiReference', { timeout: 2000 }); + }, + async expectDeleteIndexButtonExistsInMoreOptions() { await testSubjects.existOrFail('moreOptionsDeleteIndex'); }, async clickDeleteIndexButton() { diff --git a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts index b6247f90b37c5..2f190cfdf7eef 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts @@ -104,9 +104,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { my_field: [1, 0, 1], }, }); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + }); + it('menu action item should be replaced with playground', async () => { + await pageObjects.svlSearchIndexDetailPage.expectUseInPlaygroundLinkExists(); }); it('should have index documents', async () => { - await svlSearchNavigation.navigateToIndexDetailPage(indexName); await pageObjects.svlSearchIndexDetailPage.expectHasIndexDocuments(); }); it('should have with data tabs', async () => { @@ -149,8 +152,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.svlSearchIndexDetailPage.clickMoreOptionsActionsButton(); await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsOverviewMenuIsShown(); }); + it('should have link to API reference doc link', async () => { + await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkExistsInMoreOptions(); + }); + it('should have link to playground', async () => { + await pageObjects.svlSearchIndexDetailPage.expectPlaygroundButtonExistsInMoreOptions(); + }); it('should delete index', async () => { - await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonExists(); + await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonExistsInMoreOptions(); await pageObjects.svlSearchIndexDetailPage.clickDeleteIndexButton(); await pageObjects.svlSearchIndexDetailPage.clickConfirmingDeleteIndex(); });