From 6931a41432ac03d885e19f3399d4a84fe9093e4c Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Tue, 27 Aug 2024 17:23:01 +0800 Subject: [PATCH] [Workspace]Essential/Analytics(All) use case overview page (#7673) * navigates to use case overview page Signed-off-by: Hailong Cui * Essential use case overviea page Signed-off-by: Hailong Cui Changeset file for PR #7673 created/updated Signed-off-by: Hailong Cui Revert "navigates to use case overview page" This reverts commit a43f533c4086cc742bd018ffa7a79a909e508897. Signed-off-by: Hailong Cui * set breadcrumb for overview page Signed-off-by: Hailong Cui * remove width Signed-off-by: Hailong Cui * Analytics use case overview Signed-off-by: Hailong Cui * add unit test Signed-off-by: Hailong Cui * Changeset file for PR #7673 created/updated * add cardProps for card input Signed-off-by: Hailong Cui * remove repeat license header Signed-off-by: Hailong Cui * navigates to use case overvie page instead of workspace detail Signed-off-by: Hailong Cui * Update src/plugins/content_management/public/components/card_container/card_embeddable.tsx Co-authored-by: SuZhou-Joe Signed-off-by: Hailong Cui * update page id to match pattern usecaseId_overview Signed-off-by: Hailong Cui * fix import sample data target area Signed-off-by: Hailong Cui * workspace icon Signed-off-by: Hailong Cui * move all content ids to content management plugin Signed-off-by: Hailong Cui * refactor whats_new and learn opensearch Signed-off-by: Hailong Cui * fix merge issue Signed-off-by: Hailong Cui * fix merge issue Signed-off-by: Hailong Cui * unify use case page content id Signed-off-by: Hailong Cui * add unit test Signed-off-by: Hailong Cui --------- Signed-off-by: Hailong Cui Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Co-authored-by: SuZhou-Joe --- changelogs/fragments/7673.yml | 2 + src/core/public/index.ts | 4 + src/core/utils/default_nav_groups.ts | 17 +- src/core/utils/index.ts | 9 +- .../card_container/card_embeddable.test.tsx | 31 ++++ .../card_container/card_embeddable.tsx | 30 ++-- .../components/card_container/card_list.tsx | 7 +- .../public/components/card_container/types.ts | 2 + .../public/components/section_input.ts | 1 + .../public/components/section_render.tsx | 33 ++-- .../content_management/public/constants.ts | 55 +++++++ .../content_management/public/index.ts | 2 + .../content_management/public/mocks.ts | 1 + .../content_management_service.test.ts | 23 +++ .../content_management_service.ts | 23 +-- .../services/content_management/types.ts | 4 +- src/plugins/home/common/constants.ts | 12 -- .../public/application/components/home_app.js | 3 +- .../components/home_list_card.test.tsx | 64 +++++++- .../application/components/home_list_card.tsx | 68 ++++++++ .../sample_data/sample_data_card.test.tsx | 43 +++++ .../sample_data/sample_data_card.tsx | 45 +++++ .../home/public/application/home_render.tsx | 43 ++--- src/plugins/home/public/index.ts | 2 - src/plugins/home/public/plugin.ts | 8 + .../opensearch_dashboards.json | 2 +- .../saved_objects_management/public/plugin.ts | 12 +- src/plugins/workspace/common/constants.ts | 39 ----- .../workspace/opensearch_dashboards.json | 2 +- src/plugins/workspace/public/application.tsx | 18 ++ .../use_case_overview/get_started_cards.tsx | 73 +++++++++ .../components/use_case_overview/index.ts | 6 + .../use_case_overview/setup_overview.test.tsx | 150 +++++++++++++++++ .../use_case_overview/setup_overview.tsx | 154 ++++++++++++++++++ .../workspace_use_case_overview_app.tsx | 38 +++++ src/plugins/workspace/public/plugin.test.ts | 89 +++++++++- src/plugins/workspace/public/plugin.ts | 114 ++++++++++++- src/plugins/workspace/public/types.ts | 2 + src/plugins/workspace/public/utils.ts | 16 +- 39 files changed, 1109 insertions(+), 138 deletions(-) create mode 100644 changelogs/fragments/7673.yml create mode 100644 src/plugins/content_management/public/constants.ts create mode 100644 src/plugins/home/public/application/components/sample_data/sample_data_card.test.tsx create mode 100644 src/plugins/home/public/application/components/sample_data/sample_data_card.tsx create mode 100644 src/plugins/workspace/public/components/use_case_overview/get_started_cards.tsx create mode 100644 src/plugins/workspace/public/components/use_case_overview/index.ts create mode 100644 src/plugins/workspace/public/components/use_case_overview/setup_overview.test.tsx create mode 100644 src/plugins/workspace/public/components/use_case_overview/setup_overview.tsx create mode 100644 src/plugins/workspace/public/components/workspace_use_case_overview_app.tsx diff --git a/changelogs/fragments/7673.yml b/changelogs/fragments/7673.yml new file mode 100644 index 000000000000..f0a84647bad2 --- /dev/null +++ b/changelogs/fragments/7673.yml @@ -0,0 +1,2 @@ +feat: +- [Workspace]Essential/Analytics(All) use case overview page ([#7673](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7673)) \ No newline at end of file diff --git a/src/core/public/index.ts b/src/core/public/index.ts index a97f948a9d67..2398b16e7c03 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -112,6 +112,10 @@ export { cleanWorkspaceId, DEFAULT_NAV_GROUPS, ALL_USE_CASE_ID, + SEARCH_USE_CASE_ID, + ESSENTIAL_USE_CASE_ID, + OBSERVABILITY_USE_CASE_ID, + SECURITY_ANALYTICS_USE_CASE_ID, } from '../utils'; export { AppCategory, diff --git a/src/core/utils/default_nav_groups.ts b/src/core/utils/default_nav_groups.ts index 64fea126ff3e..7b3820ccfbd3 100644 --- a/src/core/utils/default_nav_groups.ts +++ b/src/core/utils/default_nav_groups.ts @@ -7,6 +7,10 @@ import { i18n } from '@osd/i18n'; import { ChromeNavGroup, NavGroupType } from '../types'; export const ALL_USE_CASE_ID = 'all'; +export const OBSERVABILITY_USE_CASE_ID = 'observability'; +export const SECURITY_ANALYTICS_USE_CASE_ID = 'security-analytics'; +export const ESSENTIAL_USE_CASE_ID = 'analytics'; +export const SEARCH_USE_CASE_ID = 'search'; const defaultNavGroups = { dataAdministration: { @@ -40,9 +44,10 @@ const defaultNavGroups = { defaultMessage: 'This is a use case contains all the features.', }), order: 3000, + icon: 'wsAnalytics', }, observability: { - id: 'observability', + id: OBSERVABILITY_USE_CASE_ID, title: i18n.translate('core.ui.group.observability.title', { defaultMessage: 'Observability', }), @@ -51,9 +56,10 @@ const defaultNavGroups = { 'Gain visibility into system health, performance, and reliability through monitoring and analysis of logs, metrics, and traces.', }), order: 4000, + icon: 'wsObservability', }, 'security-analytics': { - id: 'security-analytics', + id: SECURITY_ANALYTICS_USE_CASE_ID, title: i18n.translate('core.ui.group.security.analytics.title', { defaultMessage: 'Security Analytics', }), @@ -62,9 +68,10 @@ const defaultNavGroups = { 'Detect and investigate potential security threats and vulnerabilities across your systems and data.', }), order: 5000, + icon: 'wsSecurityAnalytics', }, essentials: { - id: 'analytics', + id: ESSENTIAL_USE_CASE_ID, title: i18n.translate('core.ui.group.essential.title', { defaultMessage: 'Essentials', }), @@ -73,9 +80,10 @@ const defaultNavGroups = { 'Analyze data to derive insights, identify patterns and trends, and make data-driven decisions.', }), order: 7000, + icon: 'wsEssentials', }, search: { - id: 'search', + id: SEARCH_USE_CASE_ID, title: i18n.translate('core.ui.group.search.title', { defaultMessage: 'Search', }), @@ -84,6 +92,7 @@ const defaultNavGroups = { "Quickly find and explore relevant information across your organization's data sources.", }), order: 6000, + icon: 'wsSearch', }, } as const; diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 46304c4bde3d..3cb5f43d843c 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -39,4 +39,11 @@ export { export { DEFAULT_APP_CATEGORIES } from './default_app_categories'; export { WORKSPACE_PATH_PREFIX, WORKSPACE_TYPE } from './constants'; export { getWorkspaceIdFromUrl, formatUrlWithWorkspaceId, cleanWorkspaceId } from './workspace'; -export { DEFAULT_NAV_GROUPS, ALL_USE_CASE_ID } from './default_nav_groups'; +export { + DEFAULT_NAV_GROUPS, + ALL_USE_CASE_ID, + SEARCH_USE_CASE_ID, + ESSENTIAL_USE_CASE_ID, + OBSERVABILITY_USE_CASE_ID, + SECURITY_ANALYTICS_USE_CASE_ID, +} from './default_nav_groups'; diff --git a/src/plugins/content_management/public/components/card_container/card_embeddable.test.tsx b/src/plugins/content_management/public/components/card_container/card_embeddable.test.tsx index a87cd43554ea..4a8c7a7a8f4c 100644 --- a/src/plugins/content_management/public/components/card_container/card_embeddable.test.tsx +++ b/src/plugins/content_management/public/components/card_container/card_embeddable.test.tsx @@ -28,3 +28,34 @@ test('CardEmbeddable should render a card with the title', () => { Array.from(node.querySelectorAll('*')).find((ele) => ele.textContent?.trim() === 'card title') ).toBeFalsy(); }); + +test('CardEmbeddable should render a card with the cardProps', () => { + const embeddable = new CardEmbeddable({ + id: 'card-id', + title: 'card title', + description: '', + cardProps: { + selectable: { + children: 'selectable line', + onSelect: () => {}, + }, + }, + }); + + const node = document.createElement('div'); + embeddable.render(node); + + // it should render the card with title specified + expect( + Array.from(node.querySelectorAll('*')).find( + (ele) => ele.textContent?.trim() === 'selectable line' + ) + ).toBeTruthy(); + + embeddable.destroy(); + expect( + Array.from(node.querySelectorAll('*')).find( + (ele) => ele.textContent?.trim() === 'selectable line' + ) + ).toBeFalsy(); +}); diff --git a/src/plugins/content_management/public/components/card_container/card_embeddable.tsx b/src/plugins/content_management/public/components/card_container/card_embeddable.tsx index 588ce1681957..1631b9e13959 100644 --- a/src/plugins/content_management/public/components/card_container/card_embeddable.tsx +++ b/src/plugins/content_management/public/components/card_container/card_embeddable.tsx @@ -5,7 +5,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { EuiCard } from '@elastic/eui'; +import { EuiCard, EuiCardProps } from '@elastic/eui'; import { Embeddable, EmbeddableInput, IContainer } from '../../../../embeddable/public'; @@ -15,6 +15,7 @@ export type CardEmbeddableInput = EmbeddableInput & { onClick?: () => void; getIcon?: () => React.ReactElement; getFooter?: () => React.ReactElement; + cardProps?: Omit; }; export class CardEmbeddable extends Embeddable { @@ -30,18 +31,21 @@ export class CardEmbeddable extends Embeddable { ReactDOM.unmountComponentAtNode(this.node); } this.node = node; - ReactDOM.render( - , - node - ); + + const cardProps: EuiCardProps = { + ...this.input.cardProps, + title: this.input.title ?? '', + description: this.input.description, + onClick: this.input.onClick, + icon: this.input?.getIcon?.(), + }; + + if (!cardProps.layout || cardProps.layout === 'vertical') { + cardProps.textAlign = 'left'; + cardProps.footer = this.input?.getFooter?.(); + } + + ReactDOM.render(, node); } public destroy() { 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 37cf18adb048..2ab12a3ea084 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 @@ -47,7 +47,12 @@ const CardListInner = ({ embeddable, input, embeddableServices }: Props) => { const child = embeddable.getChild(panel.explicitInput.id); return ( - + ); }); 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 8f4ea7855cab..4ddaf132ca93 100644 --- a/src/plugins/content_management/public/components/card_container/types.ts +++ b/src/plugins/content_management/public/components/card_container/types.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { EuiCardProps } from '@elastic/eui'; import { ContainerInput } from '../../../../embeddable/public'; export interface CardExplicitInput { @@ -11,6 +12,7 @@ export interface CardExplicitInput { onClick?: () => void; getIcon?: () => React.ReactElement; getFooter?: () => React.ReactElement; + cardProps?: Omit; } export type CardContainerInput = ContainerInput & { diff --git a/src/plugins/content_management/public/components/section_input.ts b/src/plugins/content_management/public/components/section_input.ts index 7d49f8a17ef9..3ae47b84881d 100644 --- a/src/plugins/content_management/public/components/section_input.ts +++ b/src/plugins/content_management/public/components/section_input.ts @@ -49,6 +49,7 @@ export const createCardInput = ( onClick: content.onClick, getIcon: content?.getIcon, getFooter: content?.getFooter, + cardProps: content.cardProps, }, }; } diff --git a/src/plugins/content_management/public/components/section_render.tsx b/src/plugins/content_management/public/components/section_render.tsx index d28fbad7296a..457b07cb7822 100644 --- a/src/plugins/content_management/public/components/section_render.tsx +++ b/src/plugins/content_management/public/components/section_render.tsx @@ -41,7 +41,12 @@ const DashboardSection = ({ section, embeddable, contents$, savedObjectsClient } if (section.kind === 'dashboard' && factory && input) { // const input = createDashboardSection(section, contents ?? []); - return ; + return ( + // to make dashboard section align with others add margin left and right -8px +
+ +
+ ); } return null; @@ -61,18 +66,20 @@ const CardSection = ({ section, embeddable, contents$ }: Props) => { if (section.kind === 'card' && factory && input) { return ( - - -

- - {section.title} -

-
+ + {section.title ? ( + +

+ + {section.title} +

+
+ ) : null} {isCardVisible && ( <> diff --git a/src/plugins/content_management/public/constants.ts b/src/plugins/content_management/public/constants.ts new file mode 100644 index 000000000000..e1d4ab07ecbc --- /dev/null +++ b/src/plugins/content_management/public/constants.ts @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ESSENTIAL_USE_CASE_ID, + ALL_USE_CASE_ID, + SEARCH_USE_CASE_ID, + OBSERVABILITY_USE_CASE_ID, + SECURITY_ANALYTICS_USE_CASE_ID, +} from '../../../core/public'; + +// central place for all content ids rendered by content management + +// page ids +export const ANALYTICS_ALL_OVERVIEW_PAGE_ID = `${ALL_USE_CASE_ID}_overview`; +export const ESSENTIAL_OVERVIEW_PAGE_ID = `${ESSENTIAL_USE_CASE_ID}_overview`; +export const SEARCH_OVERVIEW_PAGE_ID = `${SEARCH_USE_CASE_ID}_overview`; +export const OBSERVABILITY_OVERVIEW_PAGE_ID = `${OBSERVABILITY_USE_CASE_ID}_overview`; +export const SECURITY_ANALYTICS_OVERVIEW_PAGE_ID = `${SECURITY_ANALYTICS_USE_CASE_ID}_overview`; +export const HOME_PAGE_ID = 'osd_homepage'; + +// section ids +export enum SECTIONS { + GET_STARTED = `get_started`, + SERVICE_CARDS = `service_cards`, + RECENTLY_VIEWED = `recently_viewed`, + DIFFERENT_SEARCH_TYPES = 'different_search_types', + CONFIG_EVALUATE_SEARCH = 'config_evaluate_search', +} + +export enum HOME_CONTENT_AREAS { + GET_STARTED = `${HOME_PAGE_ID}/${SECTIONS.GET_STARTED}`, + SERVICE_CARDS = `${HOME_PAGE_ID}/${SECTIONS.SERVICE_CARDS}`, + RECENTLY_VIEWED = `${HOME_PAGE_ID}/${SECTIONS.RECENTLY_VIEWED}`, +} + +export enum ESSENTIAL_OVERVIEW_CONTENT_AREAS { + GET_STARTED = `${ESSENTIAL_OVERVIEW_PAGE_ID}/${SECTIONS.GET_STARTED}`, + SERVICE_CARDS = `${ESSENTIAL_OVERVIEW_PAGE_ID}/${SECTIONS.SERVICE_CARDS}`, + RECENTLY_VIEWED = `${ESSENTIAL_OVERVIEW_PAGE_ID}/${SECTIONS.RECENTLY_VIEWED}`, +} + +export enum ANALYTICS_ALL_OVERVIEW_CONTENT_AREAS { + GET_STARTED = `${ANALYTICS_ALL_OVERVIEW_PAGE_ID}/${SECTIONS.GET_STARTED}`, + SERVICE_CARDS = `${ANALYTICS_ALL_OVERVIEW_PAGE_ID}/${SECTIONS.SERVICE_CARDS}`, + RECENTLY_VIEWED = `${ANALYTICS_ALL_OVERVIEW_PAGE_ID}/${SECTIONS.RECENTLY_VIEWED}`, +} + +export enum SEARCH_OVERVIEW_CONTENT_AREAS { + DIFFERENT_SEARCH_TYPES = `${SEARCH_OVERVIEW_PAGE_ID}/${SECTIONS.DIFFERENT_SEARCH_TYPES}`, + CONFIG_EVALUATE_SEARCH = `${SEARCH_OVERVIEW_PAGE_ID}/${SECTIONS.CONFIG_EVALUATE_SEARCH}`, + GET_STARTED = `${SEARCH_OVERVIEW_PAGE_ID}/${SECTIONS.GET_STARTED}`, +} diff --git a/src/plugins/content_management/public/index.ts b/src/plugins/content_management/public/index.ts index c453782a7a9b..029e778d559b 100644 --- a/src/plugins/content_management/public/index.ts +++ b/src/plugins/content_management/public/index.ts @@ -13,3 +13,5 @@ export const plugin = (initializerContext: PluginInitializerContext) => export * from './components'; export * from './mocks'; +export * from './services/content_management'; +export * from './constants'; diff --git a/src/plugins/content_management/public/mocks.ts b/src/plugins/content_management/public/mocks.ts index 8f99be090231..79581c8e03dd 100644 --- a/src/plugins/content_management/public/mocks.ts +++ b/src/plugins/content_management/public/mocks.ts @@ -9,6 +9,7 @@ const createStartContract = (): ContentManagementPluginStart => { return { registerContentProvider: jest.fn(), renderPage: jest.fn(), + updatePageSection: jest.fn(), }; }; diff --git a/src/plugins/content_management/public/services/content_management/content_management_service.test.ts b/src/plugins/content_management/public/services/content_management/content_management_service.test.ts index b68157838b09..442b608513bb 100644 --- a/src/plugins/content_management/public/services/content_management/content_management_service.test.ts +++ b/src/plugins/content_management/public/services/content_management/content_management_service.test.ts @@ -47,6 +47,29 @@ test('it register content provider', () => { expect(cms.getPage('page1')?.getContents('section1')).toHaveLength(1); }); +test('it register content provider to multiple destination', () => { + const cms = new ContentManagementService(); + cms.registerPage({ id: 'page1', sections: [{ id: 'section1', kind: 'card', order: 0 }] }); + cms.registerPage({ id: 'page2', sections: [{ id: 'section1', kind: 'card', order: 0 }] }); + cms.registerContentProvider({ + id: 'content_provider1', + getTargetArea() { + return ['page1/section1', 'page2/section1']; + }, + getContent() { + return { + kind: 'card', + id: 'content1', + title: 'card', + description: 'descriptions', + order: 0, + }; + }, + }); + expect(cms.getPage('page1')?.getContents('section1')).toHaveLength(1); + expect(cms.getPage('page2')?.getContents('section1')).toHaveLength(1); +}); + test('it should throw error when register content provider with invalid target area', () => { const cms = new ContentManagementService(); cms.registerPage({ id: 'page1', sections: [{ id: 'section1', kind: 'card', order: 0 }] }); diff --git a/src/plugins/content_management/public/services/content_management/content_management_service.ts b/src/plugins/content_management/public/services/content_management/content_management_service.ts index 1c12ad826f56..c1a2f84a54d0 100644 --- a/src/plugins/content_management/public/services/content_management/content_management_service.ts +++ b/src/plugins/content_management/public/services/content_management/content_management_service.ts @@ -35,16 +35,19 @@ export class ContentManagementService { registerContentProvider = (provider: ContentProvider) => { this.contentProviders.set(provider.id, provider); - const targetArea = provider.getTargetArea(); - const [pageId, sectionId] = targetArea.split('/'); - - if (!pageId || !sectionId) { - throw new Error('getTargetArea() should return a string in format {pageId}/{sectionId}'); - } - - const page = this.getPage(pageId); - if (page) { - page.addContent(sectionId, provider.getContent()); + const area = provider.getTargetArea(); + const targetAreas: string[] = Array.isArray(area) ? [...area] : [area]; + for (const targetArea of targetAreas) { + const [pageId, sectionId] = targetArea.split('/'); + + if (!pageId || !sectionId) { + throw new Error('getTargetArea() should return a string in format {pageId}/{sectionId}'); + } + + const page = this.getPage(pageId); + if (page) { + page.addContent(sectionId, provider.getContent()); + } } }; 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 755e6f426a0a..11253b1138e2 100644 --- a/src/plugins/content_management/public/services/content_management/types.ts +++ b/src/plugins/content_management/public/services/content_management/types.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { EuiCardProps } from '@elastic/eui'; import { CardContainerExplicitInput } from '../../components/card_container/types'; import { DashboardContainerExplicitInput } from '../../components/types'; @@ -74,6 +75,7 @@ export type Content = onClick?: () => void; getIcon?: () => React.ReactElement; getFooter?: () => React.ReactElement; + cardProps?: Omit; }; export type SavedObjectInput = @@ -95,5 +97,5 @@ export type SavedObjectInput = export interface ContentProvider { id: string; getContent: () => Content; - getTargetArea: () => string; + getTargetArea: () => string | string[]; } diff --git a/src/plugins/home/common/constants.ts b/src/plugins/home/common/constants.ts index 6d5c74267be0..25c78c59c4ac 100644 --- a/src/plugins/home/common/constants.ts +++ b/src/plugins/home/common/constants.ts @@ -32,15 +32,3 @@ export const PLUGIN_ID = 'home'; export const HOME_APP_BASE_PATH = `/app/${PLUGIN_ID}`; export const USE_NEW_HOME_PAGE = 'home:useNewHomePage'; export const IMPORT_SAMPLE_DATA_APP_ID = 'import_sample_data'; -export const HOME_PAGE_ID = 'osd_homepage'; -export enum SECTIONS { - GET_STARTED = `get_started`, - SERVICE_CARDS = `service_cards`, - RECENTLY_VIEWED = `recently_viewed`, -} - -export enum HOME_CONTENT_AREAS { - GET_STARTED = `${HOME_PAGE_ID}/${SECTIONS.GET_STARTED}`, - SERVICE_CARDS = `${HOME_PAGE_ID}/${SECTIONS.SERVICE_CARDS}`, - RECENTLY_VIEWED = `${HOME_PAGE_ID}/${SECTIONS.RECENTLY_VIEWED}`, -} diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 4eb253a9cdd0..61596c724310 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -40,7 +40,8 @@ import { getTutorial } from '../load_tutorials'; import { replaceTemplateStrings } from './tutorial/replace_template_strings'; import { getServices } from '../opensearch_dashboards_services'; import { useMount } from 'react-use'; -import { USE_NEW_HOME_PAGE, HOME_PAGE_ID } from '../../../common/constants'; +import { USE_NEW_HOME_PAGE } from '../../../common/constants'; +import { HOME_PAGE_ID } from '../../../../content_management/public'; const RedirectToDefaultApp = () => { useMount(() => { diff --git a/src/plugins/home/public/application/components/home_list_card.test.tsx b/src/plugins/home/public/application/components/home_list_card.test.tsx index 8558c62727a4..f5916e4b74e7 100644 --- a/src/plugins/home/public/application/components/home_list_card.test.tsx +++ b/src/plugins/home/public/application/components/home_list_card.test.tsx @@ -6,7 +6,8 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { HomeListCard } from './home_list_card'; +import { HomeListCard, registerHomeListCardToPage } from './home_list_card'; +import { contentManagementPluginMocks } from '../../../../content_management/public'; describe('', () => { it('should render static content normally', async () => { @@ -60,3 +61,64 @@ it('should not show View All button when allLink is not provided', () => { const { queryByText } = render(); expect(queryByText('View all')).not.toBeInTheDocument(); }); + +describe('Register HomeListCardToPages', () => { + const registerContentProviderFn = jest.fn(); + const contentManagementStartMock = { + ...contentManagementPluginMocks.createStartContract(), + registerContentProvider: registerContentProviderFn, + }; + + it('register to use case overview page', () => { + registerHomeListCardToPage(contentManagementStartMock); + expect(contentManagementStartMock.registerContentProvider).toHaveBeenCalledTimes(4); + + let whatsNewCall = registerContentProviderFn.mock.calls[0]; + expect(whatsNewCall[0].getTargetArea()).toEqual('analytics_overview/service_cards'); + expect(whatsNewCall[0].getContent()).toMatchInlineSnapshot(` + Object { + "id": "whats_new", + "kind": "custom", + "order": 10, + "render": [Function], + "width": 24, + } + `); + + let learnOpenSearchCall = registerContentProviderFn.mock.calls[1]; + expect(learnOpenSearchCall[0].getTargetArea()).toEqual('analytics_overview/service_cards'); + expect(learnOpenSearchCall[0].getContent()).toMatchInlineSnapshot(` + Object { + "id": "learn_opensearch_new", + "kind": "custom", + "order": 20, + "render": [Function], + "width": 24, + } + `); + + whatsNewCall = registerContentProviderFn.mock.calls[2]; + expect(whatsNewCall[0].getTargetArea()).toEqual('all_overview/service_cards'); + expect(whatsNewCall[0].getContent()).toMatchInlineSnapshot(` + Object { + "id": "whats_new", + "kind": "custom", + "order": 30, + "render": [Function], + "width": undefined, + } + `); + + learnOpenSearchCall = registerContentProviderFn.mock.calls[3]; + expect(learnOpenSearchCall[0].getTargetArea()).toEqual('all_overview/service_cards'); + expect(learnOpenSearchCall[0].getContent()).toMatchInlineSnapshot(` + Object { + "id": "learn_opensearch_new", + "kind": "custom", + "order": 40, + "render": [Function], + "width": undefined, + } + `); + }); +}); diff --git a/src/plugins/home/public/application/components/home_list_card.tsx b/src/plugins/home/public/application/components/home_list_card.tsx index e584635c680e..2d1cee86f838 100644 --- a/src/plugins/home/public/application/components/home_list_card.tsx +++ b/src/plugins/home/public/application/components/home_list_card.tsx @@ -17,6 +17,11 @@ import { EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; +import { + ANALYTICS_ALL_OVERVIEW_CONTENT_AREAS, + ContentManagementPluginStart, + ESSENTIAL_OVERVIEW_CONTENT_AREAS, +} from '../../../../content_management/public'; export const LEARN_OPENSEARCH_CONFIG = { title: i18n.translate('homepage.card.learnOpenSearch.title', { @@ -116,3 +121,66 @@ export const HomeListCard = ({ config }: { config: Config }) => { ); }; + +export const registerHomeListCard = ( + contentManagement: ContentManagementPluginStart, + { + target, + order, + width, + config, + id, + }: { + target: string; + order: number; + width?: number; + config: Config; + id: string; + } +) => { + contentManagement.registerContentProvider({ + id: `${id}_${target}_cards`, + getContent: () => ({ + id, + kind: 'custom', + order, + width, + render: () => + React.createElement(HomeListCard, { + config, + }), + }), + getTargetArea: () => target, + }); +}; +export const registerHomeListCardToPage = (contentManagement: ContentManagementPluginStart) => { + registerHomeListCard(contentManagement, { + id: 'whats_new', + order: 10, + config: WHATS_NEW_CONFIG, + target: ESSENTIAL_OVERVIEW_CONTENT_AREAS.SERVICE_CARDS, + width: 24, + }); + + registerHomeListCard(contentManagement, { + id: 'learn_opensearch_new', + order: 20, + config: LEARN_OPENSEARCH_CONFIG, + target: ESSENTIAL_OVERVIEW_CONTENT_AREAS.SERVICE_CARDS, + width: 24, + }); + + registerHomeListCard(contentManagement, { + id: 'whats_new', + order: 30, + config: WHATS_NEW_CONFIG, + target: ANALYTICS_ALL_OVERVIEW_CONTENT_AREAS.SERVICE_CARDS, + }); + + registerHomeListCard(contentManagement, { + id: 'learn_opensearch_new', + order: 40, + config: LEARN_OPENSEARCH_CONFIG, + target: ANALYTICS_ALL_OVERVIEW_CONTENT_AREAS.SERVICE_CARDS, + }); +}; 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 new file mode 100644 index 000000000000..44d7a4b6f007 --- /dev/null +++ b/src/plugins/home/public/application/components/sample_data/sample_data_card.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { registerSampleDataCard } from './sample_data_card'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { contentManagementPluginMocks } from '../../../../../content_management/public'; + +describe('Sample data card', () => { + const coreStart = coreMock.createStart(); + const registerContentProviderMock = jest.fn(); + + const contentManagement = { + ...contentManagementPluginMocks.createStartContract(), + registerContentProvider: registerContentProviderMock, + }; + + it('should call the getTargetArea function with the correct arguments', () => { + registerSampleDataCard(contentManagement, coreStart); + const call = registerContentProviderMock.mock.calls[0]; + expect(call[0].getTargetArea()).toEqual(['analytics_overview/get_started']); + expect(call[0].getContent()).toMatchInlineSnapshot(` + Object { + "cardProps": Object { + "selectable": Object { + "children": , + "isSelected": false, + "onClick": [Function], + }, + }, + "description": "You can install sample data to experiment with OpenSearch Dashboards.", + "id": "sample_data", + "kind": "card", + "order": 0, + "title": "Try openSearch", + } + `); + }); +}); 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 new file mode 100644 index 000000000000..4deb4eb8ff59 --- /dev/null +++ b/src/plugins/home/public/application/components/sample_data/sample_data_card.tsx @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CoreStart } from 'opensearch-dashboards/public'; +import React from 'react'; +import { EuiI18n } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { + ContentManagementPluginStart, + ESSENTIAL_OVERVIEW_CONTENT_AREAS, +} from '../../../../../content_management/public'; +import { IMPORT_SAMPLE_DATA_APP_ID } from '../../../../common/constants'; + +export const registerSampleDataCard = ( + contentManagement: ContentManagementPluginStart, + core: CoreStart +) => { + contentManagement.registerContentProvider({ + id: `get_start_sample_data`, + getTargetArea: () => [ESSENTIAL_OVERVIEW_CONTENT_AREAS.GET_STARTED], + getContent: () => ({ + id: 'sample_data', + kind: 'card', + order: 0, + description: i18n.translate('home.sampleData.card.description', { + defaultMessage: 'You can install sample data to experiment with OpenSearch Dashboards.', + }), + title: i18n.translate('home.sampleData.card.title', { + defaultMessage: 'Try openSearch', + }), + cardProps: { + selectable: { + children: , + isSelected: false, + onClick: () => { + // TODO change to a modal + core.application.navigateToApp(IMPORT_SAMPLE_DATA_APP_ID); + }, + }, + }, + }), + }); +}; diff --git a/src/plugins/home/public/application/home_render.tsx b/src/plugins/home/public/application/home_render.tsx index afddae56ce63..03babca342eb 100644 --- a/src/plugins/home/public/application/home_render.tsx +++ b/src/plugins/home/public/application/home_render.tsx @@ -8,12 +8,14 @@ import { CoreStart } from 'opensearch-dashboards/public'; import { ContentManagementPluginSetup, ContentManagementPluginStart, + HOME_PAGE_ID, + SECTIONS, + HOME_CONTENT_AREAS, } from '../../../../plugins/content_management/public'; -import { HOME_PAGE_ID, SECTIONS, HOME_CONTENT_AREAS } from '../../common/constants'; import { WHATS_NEW_CONFIG, LEARN_OPENSEARCH_CONFIG, - HomeListCard, + registerHomeListCard, } from './components/home_list_card'; export const setupHome = (contentManagement: ContentManagementPluginSetup) => { @@ -56,30 +58,19 @@ export const setupHome = (contentManagement: ContentManagementPluginSetup) => { }; export const initHome = (contentManagement: ContentManagementPluginStart, core: CoreStart) => { - contentManagement.registerContentProvider({ - id: 'whats_new_cards', - getContent: () => ({ - id: 'whats_new', - kind: 'custom', - order: 3, - render: () => - React.createElement(HomeListCard, { - config: WHATS_NEW_CONFIG, - }), - }), - getTargetArea: () => HOME_CONTENT_AREAS.SERVICE_CARDS, + registerHomeListCard(contentManagement, { + id: 'whats_new', + order: 3, + config: WHATS_NEW_CONFIG, + target: HOME_CONTENT_AREAS.SERVICE_CARDS, + width: 16, }); - contentManagement.registerContentProvider({ - id: 'learn_opensearch_new_cards', - getContent: () => ({ - id: 'learn_opensearch', - kind: 'custom', - order: 4, - render: () => - React.createElement(HomeListCard, { - config: LEARN_OPENSEARCH_CONFIG, - }), - }), - getTargetArea: () => HOME_CONTENT_AREAS.SERVICE_CARDS, + + registerHomeListCard(contentManagement, { + id: 'learn_opensearch_new', + order: 4, + config: LEARN_OPENSEARCH_CONFIG, + target: HOME_CONTENT_AREAS.SERVICE_CARDS, + width: 16, }); }; diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index d252a31a0977..58ad10cdf04b 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -53,5 +53,3 @@ import { HomePublicPlugin } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => new HomePublicPlugin(initializerContext); - -export { HOME_PAGE_ID, HOME_CONTENT_AREAS } from '../common/constants'; diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index cf499c19ddb9..8e32537d0a0b 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -71,6 +71,8 @@ import { ContentManagementPluginStart, } 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'; @@ -231,6 +233,12 @@ export class HomePublicPlugin // initialize homepage initHome(contentManagement, core); + // register sample data card to use case overview page + registerSampleDataCard(contentManagement, core); + + // register what's new learn opensearch card to use case overview page + registerHomeListCardToPage(contentManagement); + this.featuresCatalogueRegistry.start({ capabilities }); this.sectionTypeService.start({ core, data }); diff --git a/src/plugins/saved_objects_management/opensearch_dashboards.json b/src/plugins/saved_objects_management/opensearch_dashboards.json index 9a985345b030..5b6236ad2630 100644 --- a/src/plugins/saved_objects_management/opensearch_dashboards.json +++ b/src/plugins/saved_objects_management/opensearch_dashboards.json @@ -16,5 +16,5 @@ "contentManagement" ], "extraPublicDirs": ["public/lib"], - "requiredBundles": ["opensearchDashboardsReact", "home"] + "requiredBundles": ["opensearchDashboardsReact", "home", "contentManagement"] } diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index 104507f028e2..5a0a4cfe0330 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -65,7 +65,11 @@ import { registerServices } from './register_services'; import { bootstrap } from './ui_actions_bootstrap'; import { DEFAULT_NAV_GROUPS } from '../../../core/public'; import { RecentWork } from './management_section/recent_work'; -import { HOME_CONTENT_AREAS } from '../../../plugins/home/public'; +import { + HOME_CONTENT_AREAS, + ESSENTIAL_OVERVIEW_CONTENT_AREAS, + ANALYTICS_ALL_OVERVIEW_CONTENT_AREAS, +} from '../../../plugins/content_management/public'; import { getScopedBreadcrumbs } from '../../opensearch_dashboards_react/public'; import { NavigationPublicPluginStart } from '../../../plugins/navigation/public'; @@ -236,7 +240,11 @@ export class SavedObjectsManagementPlugin }), }; }, - getTargetArea: () => HOME_CONTENT_AREAS.RECENTLY_VIEWED, + getTargetArea: () => [ + HOME_CONTENT_AREAS.RECENTLY_VIEWED, + ESSENTIAL_OVERVIEW_CONTENT_AREAS.RECENTLY_VIEWED, + ANALYTICS_ALL_OVERVIEW_CONTENT_AREAS.RECENTLY_VIEWED, + ], }); return { diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 7cabf32d14c6..735c253fd0c0 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -4,7 +4,6 @@ */ import { i18n } from '@osd/i18n'; -import { AppCategory } from '../../../core/types'; export const WORKSPACE_FATAL_ERROR_APP_ID = 'workspace_fatal_error'; export const WORKSPACE_CREATE_APP_ID = 'workspace_create'; @@ -38,44 +37,6 @@ export const PRIORITY_FOR_WORKSPACE_UI_SETTINGS_WRAPPER = -2; export const PRIORITY_FOR_WORKSPACE_CONFLICT_CONTROL_WRAPPER = -1; export const PRIORITY_FOR_PERMISSION_CONTROL_WRAPPER = 0; -export const WORKSPACE_APP_CATEGORIES: Record = Object.freeze({ - // below categories are for workspace - getStarted: { - id: 'getStarted', - label: i18n.translate('core.ui.getStarted.label', { - defaultMessage: 'Get started', - }), - order: 10000, - }, - dashboardAndReport: { - id: 'dashboardReport', - label: i18n.translate('core.ui.dashboardReport.label', { - defaultMessage: 'Dashboard and report', - }), - order: 11000, - }, - investigate: { - id: 'investigate', - label: i18n.translate('core.ui.investigate.label', { - defaultMessage: 'Investigate', - }), - order: 12000, - }, - detect: { - id: 'detect', - label: i18n.translate('core.ui.detect.label', { - defaultMessage: 'Detect', - }), - order: 13000, - }, - searchSolution: { - id: 'searchSolution', - label: i18n.translate('core.ui.searchSolution.label', { - defaultMessage: 'Build search solution', - }), - order: 14000, - }, -}); /** * * This is a temp solution to store relationships between use cases and features. diff --git a/src/plugins/workspace/opensearch_dashboards.json b/src/plugins/workspace/opensearch_dashboards.json index ac24c20d712b..9818ab60966d 100644 --- a/src/plugins/workspace/opensearch_dashboards.json +++ b/src/plugins/workspace/opensearch_dashboards.json @@ -9,5 +9,5 @@ "navigation" ], "optionalPlugins": ["savedObjectsManagement","management","dataSourceManagement","contentManagement"], - "requiredBundles": ["opensearchDashboardsReact", "home","dataSource"] + "requiredBundles": ["opensearchDashboardsReact","dataSource","contentManagement"] } diff --git a/src/plugins/workspace/public/application.tsx b/src/plugins/workspace/public/application.tsx index cbc45c938586..88a7825b6883 100644 --- a/src/plugins/workspace/public/application.tsx +++ b/src/plugins/workspace/public/application.tsx @@ -16,6 +16,7 @@ import { WorkspaceCreatorProps } from './components/workspace_creator/workspace_ import { WorkspaceDetailApp } from './components/workspace_detail_app'; import { WorkspaceDetailProps } from './components/workspace_detail/workspace_detail'; import { WorkspaceInitialApp } from './components/workspace_initial_app'; +import { WorkspaceUseCaseOverviewApp } from './components/workspace_use_case_overview_app'; export const renderCreatorApp = ( { element }: AppMountParameters, @@ -102,3 +103,20 @@ export const renderInitialApp = ({}: AppMountParameters, services: Services) => ReactDOM.unmountComponentAtNode(rootElement!); }; }; + +export const renderUseCaseOverviewApp = async ( + { element }: AppMountParameters, + services: Services, + pageId: string +) => { + ReactDOM.render( + + + , + element + ); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +}; diff --git a/src/plugins/workspace/public/components/use_case_overview/get_started_cards.tsx b/src/plugins/workspace/public/components/use_case_overview/get_started_cards.tsx new file mode 100644 index 000000000000..f4edb5466215 --- /dev/null +++ b/src/plugins/workspace/public/components/use_case_overview/get_started_cards.tsx @@ -0,0 +1,73 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiI18n } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import React from 'react'; + +interface GetStartCard { + id: string; + title: string; + description: string; + footer: React.JSX.Element; + navigateAppId: string; + order: number; +} + +const DISCOVER_APP_ID = 'discover'; +const VISUALIZE_APP_ID = 'visualize'; +const DASHBOARDS_APP_ID = 'dashboards'; + +export const getStartedCards: GetStartCard[] = [ + { + id: 'get_start_discover', + title: i18n.translate('workspace.essential_overview.discover.card.title', { + defaultMessage: 'Discover insights', + }), + description: i18n.translate('workspace.essential_overview.discover.card.description', { + defaultMessage: 'Explore data interactively to uncover insights.', + }), + footer: ( + + ), + navigateAppId: DISCOVER_APP_ID, + order: 20, + }, + { + id: 'get_start_visualization', + title: i18n.translate('workspace.essential_overview.visualize.card.title', { + defaultMessage: 'Visualize data', + }), + description: i18n.translate('workspace.essential_overview.visualize.card.description', { + defaultMessage: + 'Unlock insightful data exploration with visualization and aggregation tools.', + }), + footer: ( + + ), + navigateAppId: VISUALIZE_APP_ID, + order: 30, + }, + { + id: 'get_start_dashboards', + title: i18n.translate('workspace.essential_overview.dashboards.card.title', { + defaultMessage: 'View the big picture', + }), + description: i18n.translate('workspace.essential_overview.dashboards.card.description', { + defaultMessage: 'Gain clarity and visibility with dynamic data visualization tools.', + }), + footer: ( + + ), + navigateAppId: DASHBOARDS_APP_ID, + order: 40, + }, +]; diff --git a/src/plugins/workspace/public/components/use_case_overview/index.ts b/src/plugins/workspace/public/components/use_case_overview/index.ts new file mode 100644 index 000000000000..9691737d216d --- /dev/null +++ b/src/plugins/workspace/public/components/use_case_overview/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { setEssentialOverviewSection, registerEssentialOverviewContent } from './setup_overview'; diff --git a/src/plugins/workspace/public/components/use_case_overview/setup_overview.test.tsx b/src/plugins/workspace/public/components/use_case_overview/setup_overview.test.tsx new file mode 100644 index 000000000000..0d8733a21ce7 --- /dev/null +++ b/src/plugins/workspace/public/components/use_case_overview/setup_overview.test.tsx @@ -0,0 +1,150 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ContentManagementPluginSetup, + ContentManagementPluginStart, +} from '../../../../../plugins/content_management/public'; +import { coreMock } from '../../../../../core/public/mocks'; +import { + registerAnalyticsAllOverviewContent, + registerEssentialOverviewContent, + setEssentialOverviewSection, +} from './setup_overview'; + +describe('Setup use case overview', () => { + const coreStart = coreMock.createStart(); + const registerContentProviderMock = jest.fn(); + + const contentManagementStartMock: ContentManagementPluginStart = { + registerContentProvider: registerContentProviderMock, + renderPage: jest.fn(), + updatePageSection: jest.fn(), + }; + + const registerPageMock = jest.fn(); + const contentManagementSetupMock: ContentManagementPluginSetup = { + registerPage: registerPageMock, + }; + + beforeEach(() => { + registerContentProviderMock.mockClear(); + }); + + it('setEssentialOverviewSection', () => { + setEssentialOverviewSection(contentManagementSetupMock); + + const call = registerPageMock.mock.calls[0]; + expect(call[0]).toMatchInlineSnapshot(` + Object { + "id": "analytics_overview", + "sections": Array [ + Object { + "id": "service_cards", + "kind": "dashboard", + "order": 3000, + }, + Object { + "id": "recently_viewed", + "kind": "custom", + "order": 2000, + "render": [Function], + "title": "Recently viewed", + }, + Object { + "id": "get_started", + "kind": "card", + "order": 1000, + }, + ], + "title": "Overview", + } + `); + }); + + it('registerEssentialOverviewContent', () => { + registerEssentialOverviewContent(contentManagementStartMock, coreStart); + + const calls = registerContentProviderMock.mock.calls; + expect(calls.length).toBe(3); + + const firstCall = calls[0]; + expect(firstCall[0].getTargetArea()).toMatchInlineSnapshot(`"analytics_overview/get_started"`); + expect(firstCall[0].getContent()).toMatchInlineSnapshot(` + Object { + "cardProps": Object { + "selectable": Object { + "children": , + "isSelected": false, + "onClick": [Function], + }, + }, + "description": "Explore data interactively to uncover insights.", + "id": "get_start_discover", + "kind": "card", + "order": 20, + "title": "Discover insights", + } + `); + }); + + it('setAnalyticsAllOverviewSection', () => { + setEssentialOverviewSection(contentManagementSetupMock); + + const call = registerPageMock.mock.calls[0]; + expect(call[0]).toMatchInlineSnapshot(` + Object { + "id": "analytics_overview", + "sections": Array [ + Object { + "id": "service_cards", + "kind": "dashboard", + "order": 3000, + }, + Object { + "id": "recently_viewed", + "kind": "custom", + "order": 2000, + "render": [Function], + "title": "Recently viewed", + }, + Object { + "id": "get_started", + "kind": "card", + "order": 1000, + }, + ], + "title": "Overview", + } + `); + }); + + it('registerAnalyticsAllOverviewContent', () => { + registerAnalyticsAllOverviewContent(contentManagementStartMock, coreStart); + + const calls = registerContentProviderMock.mock.calls; + expect(calls.length).toBe(3); + + const firstCall = calls[0]; + expect(firstCall[0].getTargetArea()).toMatchInlineSnapshot(`"all_overview/get_started"`); + expect(firstCall[0].getContent()).toMatchInlineSnapshot(` + Object { + "cardProps": Object { + "layout": "horizontal", + }, + "description": "Gain visibility into system health, performance, and reliability through monitoring and analysis of logs, metrics, and traces.", + "getIcon": [Function], + "id": "observability", + "kind": "card", + "onClick": [Function], + "order": 4000, + "title": "Observability", + } + `); + }); +}); diff --git a/src/plugins/workspace/public/components/use_case_overview/setup_overview.tsx b/src/plugins/workspace/public/components/use_case_overview/setup_overview.tsx new file mode 100644 index 000000000000..4b801bf0880d --- /dev/null +++ b/src/plugins/workspace/public/components/use_case_overview/setup_overview.tsx @@ -0,0 +1,154 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { EuiIcon } from '@elastic/eui'; +import { first } from 'rxjs/operators'; +import { + ContentManagementPluginSetup, + ContentManagementPluginStart, + ANALYTICS_ALL_OVERVIEW_CONTENT_AREAS, + ANALYTICS_ALL_OVERVIEW_PAGE_ID, + ESSENTIAL_OVERVIEW_CONTENT_AREAS, + ESSENTIAL_OVERVIEW_PAGE_ID, + SECTIONS, +} from '../../../../content_management/public'; +import { getStartedCards } from './get_started_cards'; +import { DEFAULT_NAV_GROUPS } from '../../../../../core/public'; +import { Content } from '../../../../../plugins/content_management/public'; + +const recentlyViewSectionRender = (contents: Content[]) => { + return ( + <> + {contents.map((content) => { + if (content.kind === 'custom') { + return content.render(); + } + + return null; + })} + + ); +}; + +// Essential overview part +export const setEssentialOverviewSection = (contentManagement: ContentManagementPluginSetup) => { + contentManagement.registerPage({ + id: ESSENTIAL_OVERVIEW_PAGE_ID, + title: 'Overview', + sections: [ + { + id: SECTIONS.SERVICE_CARDS, + order: 3000, + kind: 'dashboard', + }, + { + id: SECTIONS.RECENTLY_VIEWED, + order: 2000, + title: 'Recently viewed', + kind: 'custom', + render: recentlyViewSectionRender, + }, + { + id: SECTIONS.GET_STARTED, + order: 1000, + kind: 'card', + }, + ], + }); +}; + +export const registerEssentialOverviewContent = ( + contentManagement: ContentManagementPluginStart, + core: CoreStart +) => { + getStartedCards.forEach((card) => { + contentManagement.registerContentProvider({ + id: card.id, + getTargetArea: () => ESSENTIAL_OVERVIEW_CONTENT_AREAS.GET_STARTED, + getContent: () => ({ + id: card.id, + kind: 'card', + order: card.order, + description: card.description, + title: card.title, + cardProps: { + selectable: { + onClick: () => { + core.application.navigateToApp(card.navigateAppId); + }, + children: card.footer, + isSelected: false, + }, + }, + }), + }); + }); +}; + +// Analytics(All) overview part +export const setAnalyticsAllOverviewSection = (contentManagement: ContentManagementPluginSetup) => { + contentManagement.registerPage({ + id: ANALYTICS_ALL_OVERVIEW_PAGE_ID, + title: 'Overview', + sections: [ + { + id: SECTIONS.SERVICE_CARDS, + order: 3000, + kind: 'dashboard', + }, + { + id: SECTIONS.RECENTLY_VIEWED, + order: 2000, + title: 'Recently viewed', + kind: 'custom', + render: recentlyViewSectionRender, + }, + { + id: SECTIONS.GET_STARTED, + order: 1000, + kind: 'card', + }, + ], + }); +}; + +export const registerAnalyticsAllOverviewContent = ( + contentManagement: ContentManagementPluginStart, + core: CoreStart +) => { + const useCaseCards = [ + DEFAULT_NAV_GROUPS.observability, + DEFAULT_NAV_GROUPS['security-analytics'], + DEFAULT_NAV_GROUPS.search, + ]; + useCaseCards.forEach((card, index) => { + contentManagement.registerContentProvider({ + id: card.id, + getTargetArea: () => ANALYTICS_ALL_OVERVIEW_CONTENT_AREAS.GET_STARTED, + getContent: () => ({ + id: card.id, + kind: 'card', + getIcon: () => + React.createElement(EuiIcon, { size: 'xl', type: card.icon || 'wsSelector' }), + order: card.order || index, + description: card.description, + title: card.title, + cardProps: { + layout: 'horizontal', + }, + onClick: async () => { + const navGroups = await core.chrome.navGroup.getNavGroupsMap$().pipe(first()).toPromise(); + const group = navGroups[card.id]; + if (group) { + const appId = group.navLinks?.[0].id; + if (appId) core.application.navigateToApp(appId); + } + }, + }), + }); + }); +}; diff --git a/src/plugins/workspace/public/components/workspace_use_case_overview_app.tsx b/src/plugins/workspace/public/components/workspace_use_case_overview_app.tsx new file mode 100644 index 000000000000..dcdcee22b729 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_use_case_overview_app.tsx @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect } from 'react'; +import { I18nProvider } from '@osd/i18n/react'; +import { useObservable } from 'react-use'; +import { EuiBreadcrumb } from '@elastic/eui'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; +import { Services } from '../types'; + +interface WorkspaceUseCaseOverviewProps { + pageId: string; +} + +export const WorkspaceUseCaseOverviewApp = (props: WorkspaceUseCaseOverviewProps) => { + const { + services: { contentManagement, workspaces, chrome }, + } = useOpenSearchDashboards(); + + const currentWorkspace = useObservable(workspaces.currentWorkspace$); + + useEffect(() => { + const breadcrumbs: EuiBreadcrumb[] = [ + { + text: currentWorkspace?.name, + }, + ]; + chrome.setBreadcrumbs(breadcrumbs); + }, [chrome, currentWorkspace]); + + const pageId = props.pageId; + + return ( + {contentManagement ? contentManagement.renderPage(pageId) : null} + ); +}; diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts index 4eaa8d54903b..3be162a4522e 100644 --- a/src/plugins/workspace/public/plugin.test.ts +++ b/src/plugins/workspace/public/plugin.test.ts @@ -180,7 +180,7 @@ describe('Workspace plugin', () => { ); }); - it('#setup should register workspace detail', async () => { + it('#setup should register workspace detail with a hidden application and not register to all nav group', async () => { const setupMock = coreMock.createSetup(); setupMock.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); const workspacePlugin = new WorkspacePlugin(); @@ -191,6 +191,18 @@ describe('Workspace plugin', () => { id: 'workspace_detail', }) ); + + // not register to all nav group + expect(setupMock.chrome.navGroup.addNavLinksToGroup).not.toHaveBeenCalledWith( + DEFAULT_NAV_GROUPS.all, + expect.arrayContaining([ + { + id: 'workspace_detail', + title: 'Overview', + order: 100, + }, + ]) + ); }); it('#setup should register workspace initial with a visible application', async () => { @@ -206,11 +218,84 @@ describe('Workspace plugin', () => { ); }); + it('#setup should register workspace essential use case when new home is disabled', async () => { + const setupMock = { + ...coreMock.createSetup(), + chrome: { + ...coreMock.createSetup().chrome, + navGroup: { + ...coreMock.createSetup().chrome.navGroup, + getNavGroupEnabled: jest.fn().mockReturnValue(false), + }, + }, + }; + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, { + contentManagement: { + registerPage: jest.fn(), + }, + }); + + expect(setupMock.application.register).not.toHaveBeenCalledWith( + expect.objectContaining({ + id: 'essential_overview', + }) + ); + expect(setupMock.application.register).not.toHaveBeenCalledWith( + expect.objectContaining({ + id: 'analytics_all_overview', + }) + ); + }); + + it('#setup should register workspace essential use case when new nav is enabled', async () => { + const setupMock = { + ...coreMock.createSetup(), + chrome: { + ...coreMock.createSetup().chrome, + navGroup: { + ...coreMock.createSetup().chrome.navGroup, + getNavGroupEnabled: jest.fn().mockReturnValue(true), + }, + }, + }; + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, { + contentManagement: { + registerPage: jest.fn(), + }, + }); + + expect(setupMock.application.register).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'analytics_overview', + }) + ); + }); + + it('#setup should register workspace analytics(All) use case when new nav is enabled', async () => { + const setupMock = { + ...coreMock.createSetup(), + chrome: { + ...coreMock.createSetup().chrome, + navGroup: { + ...coreMock.createSetup().chrome.navGroup, + getNavGroupEnabled: jest.fn().mockReturnValue(true), + }, + }, + }; + const workspacePlugin = new WorkspacePlugin(); + await workspacePlugin.setup(setupMock, { + contentManagement: { + registerPage: jest.fn(), + }, + }); + }); + it('#setup should register workspace navigation with a visible application', async () => { const setupMock = coreMock.createSetup(); const workspacePlugin = new WorkspacePlugin(); await workspacePlugin.setup(setupMock, {}); - expect(setupMock.application.register).toHaveBeenCalledWith( expect.objectContaining({ id: 'workspace_navigation', diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 1617744fa9ff..03c6b544b64d 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -38,7 +38,12 @@ import { Services, WorkspaceUseCase } from './types'; import { WorkspaceClient } from './workspace_client'; import { SavedObjectsManagementPluginSetup } from '../../../plugins/saved_objects_management/public'; import { ManagementSetup } from '../../../plugins/management/public'; -import { ContentManagementPluginStart } from '../../../plugins/content_management/public'; +import { + ANALYTICS_ALL_OVERVIEW_PAGE_ID, + ContentManagementPluginSetup, + ContentManagementPluginStart, + ESSENTIAL_OVERVIEW_PAGE_ID, +} from '../../../plugins/content_management/public'; import { WorkspaceMenu } from './components/workspace_menu/workspace_menu'; import { getWorkspaceColumn } from './components/workspace_column'; import { DataSourceManagementPluginSetup } from '../../../plugins/data_source_management/public'; @@ -55,8 +60,16 @@ import { toMountPoint } from '../../opensearch_dashboards_react/public'; import { UseCaseService } from './services/use_case_service'; import { WorkspaceListCard } from './components/service_card'; import { UseCaseFooter } from './components/home_get_start_card'; -import { HOME_CONTENT_AREAS } from '../../home/public'; import { NavigationPublicPluginStart } from '../../../plugins/navigation/public'; +import { HOME_CONTENT_AREAS } from '../../../plugins/content_management/public'; +import { + registerEssentialOverviewContent, + setEssentialOverviewSection, +} from './components/use_case_overview'; +import { + registerAnalyticsAllOverviewContent, + setAnalyticsAllOverviewSection, +} from './components/use_case_overview/setup_overview'; type WorkspaceAppType = ( params: AppMountParameters, @@ -68,6 +81,7 @@ interface WorkspacePluginSetupDeps { savedObjectsManagement?: SavedObjectsManagementPluginSetup; management?: ManagementSetup; dataSourceManagement?: DataSourceManagementPluginSetup; + contentManagement?: ContentManagementPluginSetup; } export interface WorkspacePluginStartDeps { @@ -113,7 +127,16 @@ export class WorkspacePlugin this.registeredUseCases$, ]).subscribe(([currentWorkspace, registeredUseCases]) => { if (currentWorkspace) { + const workspaceUseCase = currentWorkspace.features + ? getFirstUseCaseOfFeatureConfigs(currentWorkspace.features) + : undefined; + this.appUpdater$.next((app) => { + // essential use overview is only available in essential workspace + if (app.id === ESSENTIAL_OVERVIEW_PAGE_ID && workspaceUseCase === ALL_USE_CASE_ID) { + return { status: AppStatus.inaccessible }; + } + if (isAppAccessibleInWorkspace(app, currentWorkspace, registeredUseCases)) { return; } @@ -230,7 +253,12 @@ export class WorkspacePlugin public async setup( core: CoreSetup, - { savedObjectsManagement, management, dataSourceManagement }: WorkspacePluginSetupDeps + { + savedObjectsManagement, + management, + dataSourceManagement, + contentManagement, + }: WorkspacePluginSetupDeps ) { const workspaceClient = new WorkspaceClient(core.http, core.workspaces); await workspaceClient.init(); @@ -412,6 +440,78 @@ export class WorkspacePlugin workspaceAvailability: WorkspaceAvailability.outsideWorkspace, }); + if (core.chrome.navGroup.getNavGroupEnabled() && contentManagement) { + // workspace essential use case overview + core.application.register({ + id: ESSENTIAL_OVERVIEW_PAGE_ID, + title: '', + async mount(params: AppMountParameters) { + const { renderUseCaseOverviewApp } = await import('./application'); + const [ + coreStart, + { contentManagement: contentManagementStart }, + ] = await core.getStartServices(); + const services = { + ...coreStart, + workspaceClient, + dataSourceManagement, + contentManagement: contentManagementStart, + }; + + return renderUseCaseOverviewApp(params, services, ESSENTIAL_OVERVIEW_PAGE_ID); + }, + workspaceAvailability: WorkspaceAvailability.insideWorkspace, + }); + + core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.essentials, [ + { + id: ESSENTIAL_OVERVIEW_PAGE_ID, + order: -1, + title: i18n.translate('workspace.nav.essential_overview.title', { + defaultMessage: 'Overview', + }), + }, + ]); + + // initial the page structure + setEssentialOverviewSection(contentManagement); + + // register workspace Analytics(all) use case overview app + core.application.register({ + id: ANALYTICS_ALL_OVERVIEW_PAGE_ID, + title: '', + async mount(params: AppMountParameters) { + const { renderUseCaseOverviewApp } = await import('./application'); + const [ + coreStart, + { contentManagement: contentManagementStart }, + ] = await core.getStartServices(); + const services = { + ...coreStart, + workspaceClient, + dataSourceManagement, + contentManagement: contentManagementStart, + }; + + return renderUseCaseOverviewApp(params, services, ANALYTICS_ALL_OVERVIEW_PAGE_ID); + }, + workspaceAvailability: WorkspaceAvailability.insideWorkspace, + }); + + core.chrome.navGroup.addNavLinksToGroup(DEFAULT_NAV_GROUPS.all, [ + { + id: ANALYTICS_ALL_OVERVIEW_PAGE_ID, + order: -1, + title: i18n.translate('workspace.nav.analyticsAll_overview.title', { + defaultMessage: 'Overview', + }), + }, + ]); + + // initial the page structure + setAnalyticsAllOverviewSection(contentManagement); + } + /** * register workspace column into saved objects table */ @@ -447,7 +547,7 @@ export class WorkspacePlugin useCases.forEach((useCase, index) => { contentManagement.registerContentProvider({ id: `home_get_start_${useCase.id}`, - getTargetArea: () => HOME_CONTENT_AREAS.GET_STARTED, + getTargetArea: () => [HOME_CONTENT_AREAS.GET_STARTED], getContent: () => ({ id: useCase.id, kind: 'card', @@ -509,6 +609,12 @@ export class WorkspacePlugin // set breadcrumbs enricher for workspace this.breadcrumbsSubscription = enrichBreadcrumbsWithWorkspace(core); + + // register content to essential overview page + registerEssentialOverviewContent(contentManagement, core); + + // register content to analytics(All) overview page + registerAnalyticsAllOverviewContent(contentManagement, core); } return {}; } diff --git a/src/plugins/workspace/public/types.ts b/src/plugins/workspace/public/types.ts index 6d47262bbdd9..bec84221e2f1 100644 --- a/src/plugins/workspace/public/types.ts +++ b/src/plugins/workspace/public/types.ts @@ -7,12 +7,14 @@ import { CoreStart } from '../../../core/public'; import { WorkspaceClient } from './workspace_client'; import { DataSourceManagementPluginSetup } from '../../../plugins/data_source_management/public'; import { NavigationPublicPluginStart } from '../../../plugins/navigation/public'; +import { ContentManagementPluginStart } from '../../../plugins/content_management/public'; import { DataSourceAttributes } from '../../../plugins/data_source/common/data_sources'; export type Services = CoreStart & { workspaceClient: WorkspaceClient; dataSourceManagement?: DataSourceManagementPluginSetup; navigationUI?: NavigationPublicPluginStart['ui']; + contentManagement?: ContentManagementPluginStart; }; export interface WorkspaceUseCaseFeature { diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 543a79fbc4a3..01969d15159c 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -13,6 +13,7 @@ import { ChromeBreadcrumb, ApplicationStart, HttpSetup, + DEFAULT_NAV_GROUPS, } from '../../../core/public'; import { App, @@ -27,6 +28,10 @@ import { WORKSPACE_DETAIL_APP_ID } from '../common/constants'; import { WorkspaceUseCase, WorkspaceUseCaseFeature } from './types'; import { formatUrlWithWorkspaceId } from '../../../core/public/utils'; import { SigV4ServiceName } from '../../../plugins/data_source/common/data_sources'; +import { + ANALYTICS_ALL_OVERVIEW_PAGE_ID, + ESSENTIAL_OVERVIEW_PAGE_ID, +} from '../../../plugins/content_management/public'; export const USE_CASE_PREFIX = 'use-case-'; @@ -325,7 +330,11 @@ export function prependWorkspaceToBreadcrumbs( currentNavGroup: NavGroupItemInMap | undefined, navGroupsMap: Record ) { - if (appId === WORKSPACE_DETAIL_APP_ID) { + if ( + appId === WORKSPACE_DETAIL_APP_ID || + appId === ESSENTIAL_OVERVIEW_PAGE_ID || + appId === ANALYTICS_ALL_OVERVIEW_PAGE_ID + ) { core.chrome.setBreadcrumbsEnricher(undefined); return; } @@ -371,7 +380,7 @@ export function prependWorkspaceToBreadcrumbs( }, }; if (useCase === ALL_USE_CASE_ID) { - if (currentNavGroup) { + if (currentNavGroup && currentNavGroup.id !== DEFAULT_NAV_GROUPS.all.id) { return [homeBreadcrumb, workspaceBreadcrumb, navGroupBreadcrumb, ...breadcrumbs]; } else { return [homeBreadcrumb, workspaceBreadcrumb, ...breadcrumbs]; @@ -389,8 +398,7 @@ export const getUseCaseUrl = ( application: ApplicationStart, http: HttpSetup ): string => { - const appId = - (useCase?.id !== ALL_USE_CASE_ID && useCase?.features?.[0].id) || WORKSPACE_DETAIL_APP_ID; + const appId = useCase?.features?.[0].id || WORKSPACE_DETAIL_APP_ID; const useCaseURL = formatUrlWithWorkspaceId( application.getUrlForApp(appId, { absolute: false,