diff --git a/changelogs/fragments/7877.yml b/changelogs/fragments/7877.yml new file mode 100644 index 000000000000..865c7596780a --- /dev/null +++ b/changelogs/fragments/7877.yml @@ -0,0 +1,2 @@ +feat: +- [Workspace] Add search use case overview page ([#7877](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7877)) \ No newline at end of file diff --git a/src/plugins/content_management/public/components/card_container/card_list.test.tsx b/src/plugins/content_management/public/components/card_container/card_list.test.tsx index fce99b3e6174..262fbeb4a71f 100644 --- a/src/plugins/content_management/public/components/card_container/card_list.test.tsx +++ b/src/plugins/content_management/public/components/card_container/card_list.test.tsx @@ -20,7 +20,7 @@ test('render list of cards', () => { jest .spyOn(embeddableStart, 'EmbeddablePanel') .mockImplementation(() => CardEmbeddablePanel); - render( + const { container, queryAllByText } = render( { } /> ); - expect(screen.queryAllByText('CardEmbeddablePanel')).toHaveLength(2); + expect(queryAllByText('CardEmbeddablePanel')).toHaveLength(2); + // verify container has div with class euiFlexGroup + expect(container.querySelector('.euiFlexGroup')).toBeInTheDocument(); +}); + +test('render list of cards with grid', () => { + const embeddableStart = embeddablePluginMock.createStartContract(); + jest + .spyOn(embeddableStart, 'EmbeddablePanel') + .mockImplementation(() => CardEmbeddablePanel); + const { container } = render( + + ); + // verify container has div with class euiFlexGrid + expect(container.querySelector('.euiFlexGrid')).toBeInTheDocument(); }); diff --git a/src/plugins/content_management/public/components/card_container/card_list.tsx b/src/plugins/content_management/public/components/card_container/card_list.tsx index 2ab12a3ea084..2bbbd87c3a54 100644 --- a/src/plugins/content_management/public/components/card_container/card_list.tsx +++ b/src/plugins/content_management/public/components/card_container/card_list.tsx @@ -4,8 +4,9 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FlexGridColumns } from '@elastic/eui/src/components/flex/flex_grid'; import { IContainer, withEmbeddableSubscription, @@ -22,13 +23,18 @@ interface Props { } const CardListInner = ({ embeddable, input, embeddableServices }: Props) => { - if (input.columns) { + if (input.columns && !input.grid) { const width = `${(1 / input.columns) * 100}%`; const cards = Object.values(input.panels).map((panel) => { const child = embeddable.getChild(panel.explicitInput.id); return ( - + ); }); @@ -57,6 +63,14 @@ const CardListInner = ({ embeddable, input, embeddableServices }: Props) => { ); }); + if (input.grid && input.columns) { + return ( + + {cards} + + ); + } + return ( {cards} diff --git a/src/plugins/content_management/public/components/card_container/types.ts b/src/plugins/content_management/public/components/card_container/types.ts index 17a3363faf02..359aa5524133 100644 --- a/src/plugins/content_management/public/components/card_container/types.ts +++ b/src/plugins/content_management/public/components/card_container/types.ts @@ -20,6 +20,7 @@ export interface CardExplicitInput { export type CardContainerInput = ContainerInput & { columns?: number; wrap?: boolean; + grid?: boolean; }; /** diff --git a/src/plugins/content_management/public/components/section_input.ts b/src/plugins/content_management/public/components/section_input.ts index c9049d4baf92..1ae3207867ad 100644 --- a/src/plugins/content_management/public/components/section_input.ts +++ b/src/plugins/content_management/public/components/section_input.ts @@ -35,6 +35,7 @@ export const createCardInput = ( viewMode: ViewMode.VIEW, columns: section.columns, wrap: section.wrap, + grid: section.grid, panels, ...section.input, }; diff --git a/src/plugins/content_management/public/services/content_management/types.ts b/src/plugins/content_management/public/services/content_management/types.ts index 06d001387d80..94e69d348ef1 100644 --- a/src/plugins/content_management/public/services/content_management/types.ts +++ b/src/plugins/content_management/public/services/content_management/types.ts @@ -39,6 +39,7 @@ export type Section = input?: CardContainerExplicitInput; columns?: number; wrap?: boolean; + grid?: boolean; }; export type Content = diff --git a/src/plugins/home/public/application/application.tsx b/src/plugins/home/public/application/application.tsx index 7b383b53ae92..fae0dbdd56e5 100644 --- a/src/plugins/home/public/application/application.tsx +++ b/src/plugins/home/public/application/application.tsx @@ -38,6 +38,8 @@ import { HomeApp, ImportSampleDataApp } from './components/home_app'; import { getServices } from './opensearch_dashboards_services'; import './index.scss'; +import { ContentManagementPluginStart } from '../../../../plugins/content_management/public'; +import { SearchUseCaseOverviewApp } from './components/usecase_overview/search_use_case_app'; export const renderApp = async ( element: HTMLElement, @@ -90,3 +92,20 @@ export const renderImportSampleDataApp = async (element: HTMLElement, coreStart: unmountComponentAtNode(element); }; }; + +export const renderSearchUseCaseOverviewApp = async ( + element: HTMLElement, + coreStart: CoreStart, + contentManagementStart: ContentManagementPluginStart +) => { + render( + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; +}; diff --git a/src/plugins/home/public/application/components/sample_data/sample_data_card.test.tsx b/src/plugins/home/public/application/components/sample_data/sample_data_card.test.tsx index df93e5cd2a4b..03552b6444b2 100644 --- a/src/plugins/home/public/application/components/sample_data/sample_data_card.test.tsx +++ b/src/plugins/home/public/application/components/sample_data/sample_data_card.test.tsx @@ -19,7 +19,7 @@ describe('Sample data card', () => { it('should call the getTargetArea function with the correct arguments', () => { registerSampleDataCard(contentManagement, coreStart); const call = registerContentProviderMock.mock.calls[0]; - expect(call[0].getTargetArea()).toEqual(['essentials_overview/get_started']); + expect(call[0].getTargetArea()).toEqual('essentials_overview/get_started'); expect(call[0].getContent()).toMatchInlineSnapshot(` Object { "cardProps": Object { @@ -39,5 +39,10 @@ describe('Sample data card', () => { "title": "Try openSearch", } `); + + // search use case overview + expect(registerContentProviderMock.mock.calls[1][0].getTargetArea()).toEqual( + 'search_overview/get_started' + ); }); }); diff --git a/src/plugins/home/public/application/components/sample_data/sample_data_card.tsx b/src/plugins/home/public/application/components/sample_data/sample_data_card.tsx index 4deb4eb8ff59..84e9191fac43 100644 --- a/src/plugins/home/public/application/components/sample_data/sample_data_card.tsx +++ b/src/plugins/home/public/application/components/sample_data/sample_data_card.tsx @@ -9,7 +9,9 @@ import { EuiI18n } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { ContentManagementPluginStart, + ContentProvider, ESSENTIAL_OVERVIEW_CONTENT_AREAS, + SEARCH_OVERVIEW_CONTENT_AREAS, } from '../../../../../content_management/public'; import { IMPORT_SAMPLE_DATA_APP_ID } from '../../../../common/constants'; @@ -17,13 +19,13 @@ export const registerSampleDataCard = ( contentManagement: ContentManagementPluginStart, core: CoreStart ) => { - contentManagement.registerContentProvider({ - id: `get_start_sample_data`, - getTargetArea: () => [ESSENTIAL_OVERVIEW_CONTENT_AREAS.GET_STARTED], + const sampleDataCard = (order: number, targetArea: string): ContentProvider => ({ + id: `get_start_sample_data_${targetArea}`, + getTargetArea: () => targetArea, getContent: () => ({ id: 'sample_data', kind: 'card', - order: 0, + order, description: i18n.translate('home.sampleData.card.description', { defaultMessage: 'You can install sample data to experiment with OpenSearch Dashboards.', }), @@ -42,4 +44,11 @@ export const registerSampleDataCard = ( }, }), }); + + contentManagement.registerContentProvider( + sampleDataCard(0, ESSENTIAL_OVERVIEW_CONTENT_AREAS.GET_STARTED) + ); + contentManagement.registerContentProvider( + sampleDataCard(30, SEARCH_OVERVIEW_CONTENT_AREAS.GET_STARTED) + ); }; diff --git a/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.test.tsx b/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.test.tsx new file mode 100644 index 000000000000..40ca5e3bbc99 --- /dev/null +++ b/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; +import { contentManagementPluginMocks } from '../../../../../content_management/public/mocks'; +import { SearchUseCaseOverviewApp } from './search_use_case_app'; +import { ContentManagementPluginStart } from '../../../../../content_management/public'; +import { BehaviorSubject } from 'rxjs'; +import { WorkspaceObject } from 'opensearch-dashboards/public'; + +describe('', () => { + const renderPageMock = jest.fn(); + renderPageMock.mockReturnValue('dummy page'); + const mock = { + ...contentManagementPluginMocks.createStartContract(), + renderPage: renderPageMock, + }; + const coreStartMocks = coreMock.createStart(); + + function renderSearchUseCaseOverviewApp( + contentManagement: ContentManagementPluginStart, + services = { ...coreStartMocks } + ) { + return ( + + + + ); + } + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('render for workspace disabled case', () => { + const { container } = render(renderSearchUseCaseOverviewApp(mock, coreStartMocks)); + + expect(container).toMatchInlineSnapshot(` +
+ dummy page +
+ `); + + expect(coreStartMocks.chrome.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'Search overview' }, + ]); + expect(mock.renderPage).toBeCalledWith('search_overview'); + }); + + it('render for workspace enabled case', () => { + const coreStartMocksWithWorkspace = { + ...coreStartMocks, + workspaces: { + ...coreStartMocks.workspaces, + currentWorkspace$: new BehaviorSubject({ + id: 'foo', + name: 'foo ws', + }), + }, + }; + + const { container } = render(renderSearchUseCaseOverviewApp(mock, coreStartMocksWithWorkspace)); + + expect(container).toMatchInlineSnapshot(` +
+ dummy page +
+ `); + + expect(coreStartMocks.chrome.setBreadcrumbs).toHaveBeenCalledWith([{ text: 'foo ws' }]); + expect(mock.renderPage).toBeCalledWith('search_overview'); + }); +}); diff --git a/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.tsx b/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.tsx new file mode 100644 index 000000000000..4a0a54042a31 --- /dev/null +++ b/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.tsx @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect } from 'react'; +import { useObservable } from 'react-use'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { EuiBreadcrumb } from '@elastic/eui'; +import { I18nProvider } from '@osd/i18n/react'; +import { i18n } from '@osd/i18n'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { + ContentManagementPluginStart, + SEARCH_OVERVIEW_PAGE_ID, +} from '../../../../../content_management/public'; + +interface Props { + contentManagement: ContentManagementPluginStart; +} + +export const SearchUseCaseOverviewApp = ({ contentManagement }: Props) => { + const { + services: { workspaces, chrome }, + } = useOpenSearchDashboards(); + + const currentWorkspace = useObservable(workspaces.currentWorkspace$); + + const title = i18n.translate('home.usecase.search.title', { defaultMessage: 'Search overview' }); + + useEffect(() => { + const breadcrumbs: EuiBreadcrumb[] = [ + { + text: currentWorkspace?.name || title, + }, + ]; + chrome.setBreadcrumbs(breadcrumbs); + }, [chrome, currentWorkspace, title]); + + return ( + + {contentManagement ? contentManagement.renderPage(SEARCH_OVERVIEW_PAGE_ID) : null} + + ); +}; diff --git a/src/plugins/home/public/application/components/usecase_overview/search_use_case_setup.test.tsx b/src/plugins/home/public/application/components/usecase_overview/search_use_case_setup.test.tsx new file mode 100644 index 000000000000..3d622b44d046 --- /dev/null +++ b/src/plugins/home/public/application/components/usecase_overview/search_use_case_setup.test.tsx @@ -0,0 +1,115 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { coreMock } from '../../../../../../core/public/mocks'; +import { contentManagementPluginMocks } from '../../../../../content_management/public/mocks'; +import { registerContentToSearchUseCasePage, setupSearchUseCase } from './search_use_case_setup'; + +describe('Search use case setup', () => { + const coreStart = coreMock.createStart(); + const registerContentProviderMock = jest.fn(); + const registerPageMock = jest.fn(); + + const contentManagementSetupMock = { + ...contentManagementPluginMocks.createSetupContract(), + registerPage: registerPageMock, + }; + + const contentManagementStartMock = { + ...contentManagementPluginMocks.createStartContract(), + registerContentProvider: registerContentProviderMock, + }; + + it('setupSearchUseCase', () => { + setupSearchUseCase(contentManagementSetupMock); + expect(registerPageMock).toHaveBeenCalledTimes(1); + + const call = registerPageMock.mock.calls[0]; + expect(call[0]).toMatchInlineSnapshot(` + Object { + "id": "search_overview", + "sections": Array [ + Object { + "id": "get_started", + "kind": "card", + "order": 1000, + "title": "Set up search", + }, + Object { + "columns": 2, + "grid": true, + "id": "different_search_types", + "kind": "card", + "order": 2000, + "title": "Try out different search techniques", + }, + Object { + "columns": 2, + "grid": true, + "id": "config_evaluate_search", + "kind": "card", + "order": 3000, + "title": "Configure and evaluate search", + }, + ], + "title": "Overview", + } + `); + }); + + it('registerContentToSearchUseCasePage', () => { + registerContentToSearchUseCasePage(contentManagementStartMock, coreStart); + + const call = registerContentProviderMock.mock.calls[0]; + expect(call[0].getTargetArea()).toEqual('search_overview/get_started'); + expect(call[0].getContent()).toMatchInlineSnapshot(` + Object { + "cardProps": Object { + "selectable": Object { + "children": , + "isSelected": false, + "onClick": [Function], + }, + }, + "description": "You can run a search using REST API or language client. For experimentation, you can also run queries interactively.", + "id": "access_search_functionality", + "kind": "card", + "order": 10, + "title": "Access search functionality", + } + `); + + // search type section + const searchTypesCall = registerContentProviderMock.mock.calls[2]; + expect(searchTypesCall[0].getTargetArea()).toEqual('search_overview/different_search_types'); + expect(searchTypesCall[0].getContent()).toMatchInlineSnapshot(` + Object { + "cardProps": Object { + "children":
+ + View Documentation + +
, + "layout": "horizontal", + }, + "description": "Lexical or keyword search matches documents based on exact words or phrases. Search the text using human-friendly query string query syntax or create complex, customizable queries using Query DSL—the OpenSearch query language.", + "getIcon": [Function], + "id": "text_search", + "kind": "card", + "order": 10, + "title": "Text search", + } + `); + }); +}); diff --git a/src/plugins/home/public/application/components/usecase_overview/search_use_case_setup.tsx b/src/plugins/home/public/application/components/usecase_overview/search_use_case_setup.tsx new file mode 100644 index 000000000000..d3a7abf8986c --- /dev/null +++ b/src/plugins/home/public/application/components/usecase_overview/search_use_case_setup.tsx @@ -0,0 +1,252 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { EuiI18n, EuiIcon, EuiLink } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { + ContentManagementPluginSetup, + ContentManagementPluginStart, + SEARCH_OVERVIEW_PAGE_ID, + SECTIONS, + SEARCH_OVERVIEW_CONTENT_AREAS, +} from '../../../../../content_management/public'; + +export const setupSearchUseCase = (contentManagement: ContentManagementPluginSetup) => { + contentManagement.registerPage({ + id: SEARCH_OVERVIEW_PAGE_ID, + title: 'Overview', + sections: [ + { + id: SECTIONS.GET_STARTED, + order: 1000, + title: i18n.translate('home.searchOverview.setupSection.title', { + defaultMessage: 'Set up search', + }), + kind: 'card', + }, + { + id: SECTIONS.DIFFERENT_SEARCH_TYPES, + order: 2000, + title: i18n.translate('home.searchOverview.differentSearchTypes.title', { + defaultMessage: 'Try out different search techniques', + }), + kind: 'card', + grid: true, + columns: 2, + }, + { + id: SECTIONS.CONFIG_EVALUATE_SEARCH, + order: 3000, + title: i18n.translate('home.searchOverview.configEvaluate.title', { + defaultMessage: 'Configure and evaluate search', + }), + kind: 'card', + grid: true, + columns: 2, + }, + ], + }); +}; + +export const registerContentToSearchUseCasePage = ( + contentManagement: ContentManagementPluginStart, + core: CoreStart +) => { + const getStartedCards = [ + { + id: 'access_search_functionality', + order: 10, + title: i18n.translate('home.searchOverview.setup.accessSearch.title', { + defaultMessage: 'Access search functionality', + }), + description: i18n.translate('home.searchOverview.setup.accessSearch.description', { + defaultMessage: + 'You can run a search using REST API or language client. For experimentation, you can also run queries interactively.', + }), + footer: ( + + ), + documentURL: 'https://opensearch.org/docs/latest/search-plugins/', + }, + { + id: 'create_document_index', + order: 20, + title: i18n.translate('home.searchOverview.setup.createDocumentIndex.title', { + defaultMessage: 'Create a document index', + }), + description: i18n.translate('home.searchOverview.setup.createDocumentIndex.description', { + defaultMessage: + 'You can create a document collection (an index) by adding documents to a new index.', + }), + footer: ( + + ), + documentURL: 'https://opensearch.org/docs/latest/getting-started/intro/', + }, + ]; + + getStartedCards.forEach((card) => { + contentManagement.registerContentProvider({ + id: card.id, + getContent: () => ({ + id: card.id, + kind: 'card', + order: card.order, + title: card.title, + description: card.description, + cardProps: { + selectable: { + onClick: () => { + window.open(card.documentURL, '_blank'); + }, + children: card.footer, + isSelected: false, + }, + }, + }), + getTargetArea: () => SEARCH_OVERVIEW_CONTENT_AREAS.GET_STARTED, + }); + }); + + const searchIcon = ; + + const searchTypeCards = [ + { + id: 'text_search', + order: 10, + title: i18n.translate('home.searchOverview.searchTypes.textSearch.title', { + defaultMessage: 'Text search', + }), + description: i18n.translate('home.searchOverview.searchTypes.textSearch.description', { + defaultMessage: + 'Lexical or keyword search matches documents based on exact words or phrases. Search the text using human-friendly query string query syntax or create complex, customizable queries using Query DSL—the OpenSearch query language.', + }), + icon: searchIcon, + footer: ( + + View Documentation + + ), + }, + { + id: 'analyzer', + order: 10, + title: i18n.translate('home.searchOverview.searchTypes.analyzer.title', { + defaultMessage: 'Analyzers', + }), + description: i18n.translate('home.searchOverview.searchTypes.analyzer.description', { + defaultMessage: + 'Analyzers prepare text for indexing. For example, HTML text typically has its tags removed. English text typically treats paint, painted, and painting as equivalent by mapping them all to the token paint. You can use OpenSearch’s extensive library of standard analyzers or create your own.', + }), + icon: searchIcon, + footer: ( + + View Documentation + + ), + }, + { + id: 'semantic_vector_search', + order: 10, + title: i18n.translate('home.searchOverview.searchTypes.semanticVectorSearch.title', { + defaultMessage: 'Semantic vector search', + }), + description: i18n.translate( + 'home.searchOverview.searchTypes.semanticVectorSearch.description', + { + defaultMessage: + 'Using semantic vector search, you can search for documents similar to your query in a vector space. With OpenSearch’s neural search capability, you specify an embedding model, and OpenSearch manages the operational complexity for you.', + } + ), + icon: searchIcon, + footer: ( + + View Documentation + + ), + }, + { + id: 'neural_sparse_search', + order: 10, + title: i18n.translate('home.searchOverview.searchTypes.neuralSparseSearch.title', { + defaultMessage: 'Neural sparse search', + }), + description: i18n.translate( + 'home.searchOverview.searchTypes.neuralSparseSearch.description', + { + defaultMessage: + 'Neural sparse search combines many of the advantages of Lexical and semantic search.', + } + ), + icon: searchIcon, + footer: ( + + View Documentation + + ), + }, + { + id: 'hybrid_search', + order: 10, + title: i18n.translate('home.searchOverview.searchTypes.hybridSearch.title', { + defaultMessage: 'Hybrid search', + }), + description: i18n.translate('home.searchOverview.searchTypes.hybridSearch.description', { + defaultMessage: + 'For many uses, lexical and semantic search are complementary: Lexical search performs better on highly specific queries, while semantic search performs better on broader queries. Hybrid search runs both search types and combines the results, generally producing better results than either one separately.', + }), + icon: searchIcon, + footer: ( + + View Documentation + + ), + }, + ]; + + searchTypeCards.forEach((card) => { + contentManagement.registerContentProvider({ + id: card.id, + getContent: () => ({ + id: card.id, + kind: 'card', + order: card.order, + title: card.title, + description: card.description, + getIcon: () => card.icon, + cardProps: { + children:
{card.footer}
, + layout: 'horizontal', + }, + }), + getTargetArea: () => SEARCH_OVERVIEW_CONTENT_AREAS.DIFFERENT_SEARCH_TYPES, + }); + }); +}; diff --git a/src/plugins/home/public/application/index.ts b/src/plugins/home/public/application/index.ts index 5bb49c2993d9..c61276a75f14 100644 --- a/src/plugins/home/public/application/index.ts +++ b/src/plugins/home/public/application/index.ts @@ -28,4 +28,8 @@ * under the License. */ -export { renderApp, renderImportSampleDataApp } from './application'; +export { + renderApp, + renderImportSampleDataApp, + renderSearchUseCaseOverviewApp, +} from './application'; diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index ac5f4c508821..59be17d70c1c 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -68,12 +68,18 @@ import { learnBasicsSection } from './application/components/homepage/sections/l import { ContentManagementPluginSetup, ContentManagementPluginStart, + SEARCH_OVERVIEW_PAGE_ID, } from '../../content_management/public'; import { initHome, setupHome } from './application/home_render'; import { registerSampleDataCard } from './application/components/sample_data/sample_data_card'; import { registerHomeListCardToPage } from './application/components/home_list_card'; import { toMountPoint } from '../../opensearch_dashboards_react/public'; import { HomeIcon } from './application/components/home_icon'; +import { + registerContentToSearchUseCasePage, + setupSearchUseCase, +} from './application/components/usecase_overview/search_use_case_setup'; +import { DEFAULT_NAV_GROUPS } from '../../../core/public'; export interface HomePluginStartDependencies { data: DataPublicPluginStart; @@ -168,6 +174,36 @@ export class HomePublicPlugin workspaceAvailability: WorkspaceAvailability.outsideWorkspace, }); + if (core.chrome.navGroup.getNavGroupEnabled()) { + // register search use case overview page + core.application.register({ + id: SEARCH_OVERVIEW_PAGE_ID, + title: 'Overview', + mount: async (params: AppMountParameters) => { + const [ + coreStart, + { contentManagement: contentManagementStart }, + ] = await core.getStartServices(); + setCommonService(); + + const { renderSearchUseCaseOverviewApp } = await import('./application'); + return await renderSearchUseCaseOverviewApp( + params.element, + coreStart, + contentManagementStart + ); + }, + }); + + // add to search group + core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.search, [ + { + id: SEARCH_OVERVIEW_PAGE_ID, + order: -1, + }, + ]); + } + // Register import sample data as a standalone app so that it is available inside workspace. core.application.register({ id: IMPORT_SAMPLE_DATA_APP_ID, @@ -211,6 +247,7 @@ export class HomePublicPlugin sectionTypes.registerSection(workWithDataSection); sectionTypes.registerSection(learnBasicsSection); setupHome(contentManagement); + setupSearchUseCase(contentManagement); return { featureCatalogue, @@ -234,6 +271,7 @@ export class HomePublicPlugin // register sample data card to use case overview page registerSampleDataCard(contentManagement, core); + registerContentToSearchUseCasePage(contentManagement, core); // register what's new learn opensearch card to use case overview page registerHomeListCardToPage(contentManagement); diff --git a/src/plugins/workspace/public/utils.test.ts b/src/plugins/workspace/public/utils.test.ts index d744218a1aba..f58273822282 100644 --- a/src/plugins/workspace/public/utils.test.ts +++ b/src/plugins/workspace/public/utils.test.ts @@ -618,7 +618,7 @@ describe('workspace utils: prependWorkspaceToBreadcrumbs', () => { expect(coreStart.chrome.setBreadcrumbsEnricher).not.toHaveBeenCalled(); }); - it('should enrich breadcrumbs when in a workspace and use workspace use case as current nav group', async () => { + it('should enrich breadcrumbs when in a workspace and add workspace name into breadcrumbs', async () => { const navGroupSearch = { id: 'search', title: 'Search', @@ -646,7 +646,7 @@ describe('workspace utils: prependWorkspaceToBreadcrumbs', () => { const breadcrumbs = [{ text: 'test app' }]; let enrichedBreadcrumbs = enricher?.(breadcrumbs); expect(enrichedBreadcrumbs).toHaveLength(3); - expect(enrichedBreadcrumbs?.[1].text).toEqual('Search'); + expect(enrichedBreadcrumbs?.[1].text).toEqual('test workspace 1'); // ignore current nav group prependWorkspaceToBreadcrumbs(coreStart, workspace, 'app1', navGroupDashboards, { @@ -661,7 +661,7 @@ describe('workspace utils: prependWorkspaceToBreadcrumbs', () => { enrichedBreadcrumbs = enricher?.(breadcrumbs); expect(enrichedBreadcrumbs).toHaveLength(3); - expect(enrichedBreadcrumbs?.[1].text).toEqual('Search'); + expect(enrichedBreadcrumbs?.[1].text).toEqual('test workspace 1'); }); it('should enrich breadcrumbs when in a workspace with all use case and use selected nav group', async () => { diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 4d0ab311d7de..559e81c9408b 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -24,6 +24,7 @@ import { WorkspaceObject, WorkspaceAvailability, } from '../../../core/public'; + import { WORKSPACE_DETAIL_APP_ID, USE_CASE_PREFIX } from '../common/constants'; import { getUseCaseFeatureConfig } from '../common/utils'; import { WorkspaceUseCase, WorkspaceUseCaseFeature } from './types'; @@ -32,6 +33,9 @@ import { SigV4ServiceName } from '../../../plugins/data_source/common/data_sourc import { ANALYTICS_ALL_OVERVIEW_PAGE_ID, ESSENTIAL_OVERVIEW_PAGE_ID, + OBSERVABILITY_OVERVIEW_PAGE_ID, + SEARCH_OVERVIEW_PAGE_ID, + SECURITY_ANALYTICS_OVERVIEW_PAGE_ID, } from '../../../plugins/content_management/public'; export const isUseCaseFeatureConfig = (featureConfig: string) => @@ -327,15 +331,18 @@ export function prependWorkspaceToBreadcrumbs( currentNavGroup: NavGroupItemInMap | undefined, navGroupsMap: Record ) { - if ( - appId === WORKSPACE_DETAIL_APP_ID || - appId === ESSENTIAL_OVERVIEW_PAGE_ID || - appId === ANALYTICS_ALL_OVERVIEW_PAGE_ID - ) { + if (appId === WORKSPACE_DETAIL_APP_ID) { core.chrome.setBreadcrumbsEnricher(undefined); return; } + const homeBreadcrumb: ChromeBreadcrumb = { + text: 'Home', + onClick: () => { + core.application.navigateToApp('home'); + }, + }; + /** * There has 3 cases * nav group is enable + workspace enable + in a workspace -> workspace enricher @@ -346,6 +353,20 @@ export function prependWorkspaceToBreadcrumbs( * so we don't need to have reset logic for workspace */ if (currentWorkspace) { + // use case overview page only show workspace name + if ( + appId === SEARCH_OVERVIEW_PAGE_ID || + appId === OBSERVABILITY_OVERVIEW_PAGE_ID || + appId === SECURITY_ANALYTICS_OVERVIEW_PAGE_ID || + appId === ESSENTIAL_OVERVIEW_PAGE_ID || + appId === ANALYTICS_ALL_OVERVIEW_PAGE_ID + ) { + core.chrome.setBreadcrumbsEnricher((breadcrumbs) => [ + homeBreadcrumb, + { text: currentWorkspace.name }, + ]); + return; + } const useCase = getFirstUseCaseOfFeatureConfigs(currentWorkspace?.features || []); // get workspace the only use case if (useCase && useCase !== ALL_USE_CASE_ID) { @@ -360,22 +381,20 @@ export function prependWorkspaceToBreadcrumbs( } }, }; - const homeBreadcrumb: ChromeBreadcrumb = { - text: 'Home', + + const workspaceBreadcrumb: ChromeBreadcrumb = { + text: currentWorkspace.name, onClick: () => { - core.application.navigateToApp('home'); + if (useCase) { + const allNavGroups = navGroupsMap[useCase]; + core.application.navigateToApp(allNavGroups?.navLinks[0].id); + } }, }; core.chrome.setBreadcrumbsEnricher((breadcrumbs) => { if (!breadcrumbs || !breadcrumbs.length) return breadcrumbs; - const workspaceBreadcrumb: ChromeBreadcrumb = { - text: currentWorkspace.name, - onClick: () => { - core.application.navigateToApp(WORKSPACE_DETAIL_APP_ID); - }, - }; if (useCase === ALL_USE_CASE_ID) { if (currentNavGroup && currentNavGroup.id !== DEFAULT_NAV_GROUPS.all.id) { return [homeBreadcrumb, workspaceBreadcrumb, navGroupBreadcrumb, ...breadcrumbs]; @@ -383,7 +402,7 @@ export function prependWorkspaceToBreadcrumbs( return [homeBreadcrumb, workspaceBreadcrumb, ...breadcrumbs]; } } else { - return [homeBreadcrumb, navGroupBreadcrumb, ...breadcrumbs]; + return [homeBreadcrumb, workspaceBreadcrumb, ...breadcrumbs]; } }); }