From 08c2a006fa47b614215cf9364f7161fc55b2b429 Mon Sep 17 00:00:00 2001 From: yuboluo Date: Sun, 21 Jul 2024 14:05:46 +0800 Subject: [PATCH] [Workspace] Register four get started cards in home page (#7333) * support get start card in home page Signed-off-by: yubonluo * Changeset file for PR #7333 created/updated * fix unit test errors Signed-off-by: yubonluo * optimize the code Signed-off-by: yubonluo --------- Signed-off-by: yubonluo Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/7333.yml | 2 + .../card_container/card_container.tsx | 7 +- .../card_container/card_embeddable.test.tsx | 9 +- .../card_container/card_embeddable.tsx | 10 +- .../public/components/page_render.tsx | 25 +- .../public/components/section_input.ts | 2 + .../public/components/section_render.tsx | 27 ++- .../services/content_management/types.ts | 2 + src/plugins/home/public/index.ts | 2 + .../workspace/opensearch_dashboards.json | 4 +- .../components/home_get_start_card/index.ts | 6 + .../use_case_footer.test.tsx | 156 +++++++++++++ .../home_get_start_card/use_case_footer.tsx | 219 ++++++++++++++++++ .../workspace_menu/workspace_menu.test.tsx | 12 - .../workspace_menu/workspace_menu.tsx | 12 +- src/plugins/workspace/public/plugin.test.ts | 22 +- src/plugins/workspace/public/plugin.ts | 52 ++++- 17 files changed, 522 insertions(+), 47 deletions(-) create mode 100644 changelogs/fragments/7333.yml create mode 100644 src/plugins/workspace/public/components/home_get_start_card/index.ts create mode 100644 src/plugins/workspace/public/components/home_get_start_card/use_case_footer.test.tsx create mode 100644 src/plugins/workspace/public/components/home_get_start_card/use_case_footer.tsx diff --git a/changelogs/fragments/7333.yml b/changelogs/fragments/7333.yml new file mode 100644 index 000000000000..09d225d51ca2 --- /dev/null +++ b/changelogs/fragments/7333.yml @@ -0,0 +1,2 @@ +feat: +- [Workspace] Register four get started cards in home page ([#7333](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7333)) \ No newline at end of file diff --git a/src/plugins/content_management/public/components/card_container/card_container.tsx b/src/plugins/content_management/public/components/card_container/card_container.tsx index 518734563607..f3784f1f5fc4 100644 --- a/src/plugins/content_management/public/components/card_container/card_container.tsx +++ b/src/plugins/content_management/public/components/card_container/card_container.tsx @@ -10,7 +10,12 @@ import { CardList } from './card_list'; export const CARD_CONTAINER = 'CARD_CONTAINER'; -export type CardContainerInput = ContainerInput<{ description: string; onClick?: () => void }>; +export type CardContainerInput = ContainerInput<{ + description: string; + onClick?: () => void; + getIcon?: () => React.ReactElement; + getFooter?: () => React.ReactElement; +}>; export class CardContainer extends Container<{}, CardContainerInput> { public readonly type = CARD_CONTAINER; 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 b335bac6d996..a87cd43554ea 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 @@ -3,10 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ +import React from 'react'; import { CardEmbeddable } from './card_embeddable'; test('CardEmbeddable should render a card with the title', () => { - const embeddable = new CardEmbeddable({ id: 'card-id', title: 'card title', description: '' }); + const embeddable = new CardEmbeddable({ + id: 'card-id', + title: 'card title', + description: '', + getIcon: () => <>icon, + getFooter: () => <>footer, + }); const node = document.createElement('div'); embeddable.render(node); 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 844cf13a777c..0e7b6b2c82e5 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 @@ -10,7 +10,12 @@ import { EuiCard } from '@elastic/eui'; import { Embeddable, EmbeddableInput, IContainer } from '../../../../embeddable/public'; export const CARD_EMBEDDABLE = 'card_embeddable'; -export type CardEmbeddableInput = EmbeddableInput & { description: string; onClick?: () => void }; +export type CardEmbeddableInput = EmbeddableInput & { + description: string; + onClick?: () => void; + getIcon: () => React.ReactElement; + getFooter: () => React.ReactElement; +}; export class CardEmbeddable extends Embeddable { public readonly type = CARD_EMBEDDABLE; @@ -27,10 +32,13 @@ export class CardEmbeddable extends Embeddable { this.node = node; ReactDOM.render( , node ); diff --git a/src/plugins/content_management/public/components/page_render.tsx b/src/plugins/content_management/public/components/page_render.tsx index 9a5211ca3a46..90d6033576bb 100644 --- a/src/plugins/content_management/public/components/page_render.tsx +++ b/src/plugins/content_management/public/components/page_render.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { useObservable } from 'react-use'; import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Page } from '../services'; import { SectionRender } from './section_render'; import { EmbeddableStart } from '../../../embeddable/public'; @@ -21,16 +22,22 @@ export const PageRender = ({ page, embeddable, savedObjectsClient }: Props) => { const sections = useObservable(page.getSections$()) || []; return ( -
+ {sections.map((section) => ( - + + + ))} -
+ ); }; diff --git a/src/plugins/content_management/public/components/section_input.ts b/src/plugins/content_management/public/components/section_input.ts index 1d37feef8ecc..00bb5b0683c7 100644 --- a/src/plugins/content_management/public/components/section_input.ts +++ b/src/plugins/content_management/public/components/section_input.ts @@ -42,6 +42,8 @@ export const createCardInput = ( title: content.title, description: content.description, onClick: content.onClick, + getIcon: content?.getIcon, + getFooter: content?.getFooter, }, }; } diff --git a/src/plugins/content_management/public/components/section_render.tsx b/src/plugins/content_management/public/components/section_render.tsx index 19f14bdb1d67..d28fbad7296a 100644 --- a/src/plugins/content_management/public/components/section_render.tsx +++ b/src/plugins/content_management/public/components/section_render.tsx @@ -6,9 +6,8 @@ import React, { useState, useEffect, useMemo } from 'react'; import { useObservable } from 'react-use'; import { BehaviorSubject } from 'rxjs'; -import { EuiTitle } from '@elastic/eui'; +import { EuiButtonIcon, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; - import { Content, Section } from '../services'; import { EmbeddableInput, EmbeddableRenderer, EmbeddableStart } from '../../../embeddable/public'; import { DashboardContainerInput } from '../../../dashboard/public'; @@ -49,6 +48,10 @@ const DashboardSection = ({ section, embeddable, contents$, savedObjectsClient } }; const CardSection = ({ section, embeddable, contents$ }: Props) => { + const [isCardVisible, setIsCardVisible] = useState(true); + const toggleCardVisibility = () => { + setIsCardVisible(!isCardVisible); + }; const contents = useObservable(contents$); const input = useMemo(() => { return createCardInput(section, contents ?? []); @@ -58,12 +61,24 @@ const CardSection = ({ section, embeddable, contents$ }: Props) => { if (section.kind === 'card' && factory && input) { return ( -
+ -

{section.title}

+

+ + {section.title} +

- -
+ {isCardVisible && ( + <> + + + )} + ); } 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 0a0020ed6254..55da19f26b87 100644 --- a/src/plugins/content_management/public/services/content_management/types.ts +++ b/src/plugins/content_management/public/services/content_management/types.ts @@ -59,6 +59,8 @@ export type Content = title: string; description: string; onClick?: () => void; + getIcon?: () => React.ReactElement; + getFooter?: () => React.ReactElement; }; export type SavedObjectInput = diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index 58ad10cdf04b..d252a31a0977 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -53,3 +53,5 @@ 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/workspace/opensearch_dashboards.json b/src/plugins/workspace/opensearch_dashboards.json index 2e9377b3bda9..99a66fb1743a 100644 --- a/src/plugins/workspace/opensearch_dashboards.json +++ b/src/plugins/workspace/opensearch_dashboards.json @@ -7,6 +7,6 @@ "savedObjects", "opensearchDashboardsReact" ], - "optionalPlugins": ["savedObjectsManagement","management","dataSourceManagement"], - "requiredBundles": ["opensearchDashboardsReact"] + "optionalPlugins": ["savedObjectsManagement","management","dataSourceManagement","contentManagement"], + "requiredBundles": ["opensearchDashboardsReact", "home"] } diff --git a/src/plugins/workspace/public/components/home_get_start_card/index.ts b/src/plugins/workspace/public/components/home_get_start_card/index.ts new file mode 100644 index 000000000000..f78300a492d3 --- /dev/null +++ b/src/plugins/workspace/public/components/home_get_start_card/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { UseCaseFooter } from './use_case_footer'; diff --git a/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.test.tsx b/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.test.tsx new file mode 100644 index 000000000000..8296a5ba8359 --- /dev/null +++ b/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.test.tsx @@ -0,0 +1,156 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { UseCaseFooter as UseCaseFooterComponent, UseCaseFooterProps } from './use_case_footer'; +import { coreMock, httpServiceMock } from '../../../../../core/public/mocks'; +import { IntlProvider } from 'react-intl'; +import { WorkspaceUseCase } from '../../types'; +import { CoreStart } from 'opensearch-dashboards/public'; +import { BehaviorSubject } from 'rxjs'; +import { WORKSPACE_USE_CASES } from '../../../common/constants'; + +describe('UseCaseFooter', () => { + // let coreStartMock: CoreStart; + const navigateToApp = jest.fn(); + const registeredUseCases$ = new BehaviorSubject([ + WORKSPACE_USE_CASES.observability, + WORKSPACE_USE_CASES['security-analytics'], + WORKSPACE_USE_CASES.analytics, + WORKSPACE_USE_CASES.search, + ]); + + const getMockCore = (isDashboardAdmin: boolean = true) => { + const coreStartMock = coreMock.createStart(); + coreStartMock.application.capabilities = { + ...coreStartMock.application.capabilities, + dashboards: { isDashboardAdmin }, + }; + coreStartMock.application = { + ...coreStartMock.application, + navigateToApp, + }; + jest.spyOn(coreStartMock.application, 'getUrlForApp').mockImplementation((appId: string) => { + return `https://test.com/app/${appId}`; + }); + return coreStartMock; + }; + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + const UseCaseFooter = (props: UseCaseFooterProps) => { + return ( + + + + ); + }; + it('renders create workspace button for admin when no workspaces within use case exist', () => { + const { getByTestId } = render( + + ); + + const button = getByTestId('useCase.footer.createWorkspace.button'); + expect(button).toBeInTheDocument(); + fireEvent.click(button); + const createWorkspaceButtonInModal = getByTestId('useCase.footer.modal.create.button'); + expect(createWorkspaceButtonInModal).toHaveAttribute( + 'href', + 'https://test.com/app/workspace_create' + ); + }); + + it('renders create workspace button for non-admin when no workspaces within use case exist', () => { + const { getByTestId } = render( + + ); + + const button = getByTestId('useCase.footer.createWorkspace.button'); + expect(button).toBeInTheDocument(); + fireEvent.click(button); + expect(screen.getByText('Unable to create workspace')).toBeInTheDocument(); + expect(screen.queryByTestId('useCase.footer.modal.create.button')).not.toBeInTheDocument(); + fireEvent.click(getByTestId('useCase.footer.modal.close.button')); + }); + + it('renders open workspace button when one workspace exists', () => { + const core = getMockCore(); + core.workspaces.workspaceList$.next([ + { id: 'workspace-1', name: 'workspace 1', features: ['use-case-observability'] }, + ]); + const { getByTestId } = render( + + ); + + const button = getByTestId('useCase.footer.openWorkspace.button'); + expect(button).toBeInTheDocument(); + expect(button).not.toBeDisabled(); + expect(button).toHaveAttribute('href', 'https://test.com/w/workspace-1/app/discover'); + }); + + it('renders select workspace popover when multiple workspaces exist', () => { + const core = getMockCore(); + core.workspaces.workspaceList$.next([ + { id: 'workspace-1', name: 'workspace 1', features: ['use-case-observability'] }, + { id: 'workspace-2', name: 'workspace 2', features: ['use-case-observability'] }, + ]); + + const originalLocation = window.location; + Object.defineProperty(window, 'location', { + value: { + assign: jest.fn(), + }, + }); + + render( + + ); + + const button = screen.getByText('Select workspace'); + expect(button).toBeInTheDocument(); + + fireEvent.click(button); + expect(screen.getByText('workspace 1')).toBeInTheDocument(); + expect(screen.getByText('workspace 2')).toBeInTheDocument(); + expect(screen.getByText('Observability Workspaces')).toBeInTheDocument(); + + const inputElement = screen.getByPlaceholderText('Search'); + expect(inputElement).toBeInTheDocument(); + fireEvent.change(inputElement, { target: { value: 'workspace 1' } }); + expect(screen.queryByText('workspace 2')).toBeNull(); + + fireEvent.click(screen.getByText('workspace 1')); + expect(window.location.assign).toHaveBeenCalledWith( + 'https://test.com/w/workspace-1/app/discover' + ); + Object.defineProperty(window, 'location', { + value: originalLocation, + }); + }); +}); diff --git a/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.tsx b/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.tsx new file mode 100644 index 000000000000..fddd542f64d7 --- /dev/null +++ b/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.tsx @@ -0,0 +1,219 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiText, + EuiModal, + EuiTitle, + EuiPanel, + EuiAvatar, + EuiSpacer, + EuiButton, + EuiPopover, + EuiFlexItem, + EuiModalBody, + EuiFlexGroup, + EuiFieldSearch, + EuiModalFooter, + EuiModalHeader, + EuiContextMenu, + EuiModalHeaderTitle, +} from '@elastic/eui'; +import React, { useMemo, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { i18n } from '@osd/i18n'; +import { BehaviorSubject } from 'rxjs'; +import { WORKSPACE_DETAIL_APP_ID } from '../../../common/constants'; +import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils'; +import { CoreStart, WorkspaceObject } from '../../../../../core/public'; +import { WorkspaceUseCase } from '../../types'; +import { getUseCaseFromFeatureConfig } from '../../utils'; + +export interface UseCaseFooterProps { + useCaseId: string; + useCaseTitle: string; + core: CoreStart; + registeredUseCases$: BehaviorSubject; +} + +export const UseCaseFooter = ({ + useCaseId, + useCaseTitle, + core, + registeredUseCases$, +}: UseCaseFooterProps) => { + const workspaceList = core.workspaces.workspaceList$.getValue(); + const availableUseCases = registeredUseCases$.getValue(); + const basePath = core.http.basePath; + const isDashboardAdmin = core.application.capabilities?.dashboards?.isDashboardAdmin !== false; + const [isPopoverOpen, setPopover] = useState(false); + const [searchValue, setSearchValue] = useState(''); + const [isModalVisible, setIsModalVisible] = useState(false); + const closeModal = () => setIsModalVisible(false); + const showModal = () => setIsModalVisible(!isModalVisible); + const onButtonClick = () => setPopover(!isPopoverOpen); + const closePopover = () => setPopover(false); + + const appId = + availableUseCases?.find((useCase) => useCase.id === useCaseId)?.features[0] ?? + WORKSPACE_DETAIL_APP_ID; + + const filterWorkspaces = useMemo( + () => + workspaceList.filter( + (workspace) => + workspace.features?.map(getUseCaseFromFeatureConfig).filter(Boolean)[0] === useCaseId + ), + [useCaseId, workspaceList] + ); + + const searchWorkspaces = useMemo( + () => + filterWorkspaces + .filter((workspace) => workspace.name.toLowerCase().includes(searchValue.toLowerCase())) + .slice(0, 5), + [filterWorkspaces, searchValue] + ); + + if (filterWorkspaces.length === 0) { + const modalHeaderTitle = i18n.translate('useCase.footer.modal.headerTitle', { + defaultMessage: isDashboardAdmin ? 'No workspaces found' : 'Unable to create workspace', + }); + const modalBodyContent = i18n.translate('useCase.footer.modal.bodyContent', { + defaultMessage: isDashboardAdmin + ? 'There are no available workspaces found. You can create a workspace in the workspace creation page.' + : 'To create a workspace, contact your administrator.', + }); + + return ( + <> + + + + {isModalVisible && ( + + + {modalHeaderTitle} + + + + {modalBodyContent} + + + + + + + {isDashboardAdmin && ( + + + + )} + + + )} + + ); + } + + if (filterWorkspaces.length === 1) { + const useCaseURL = formatUrlWithWorkspaceId( + core.application.getUrlForApp(appId, { absolute: false }), + filterWorkspaces[0].id, + basePath + ); + return ( + + + + ); + } + + const workspaceToItem = (workspace: WorkspaceObject) => { + const useCaseURL = formatUrlWithWorkspaceId( + core.application.getUrlForApp(appId, { absolute: false }), + workspace.id, + basePath + ); + const workspaceName = workspace.name; + + return { + toolTipContent:
{workspaceName}
, + name: ( + + {workspaceName} + + ), + key: workspace.id, + icon: ( + + ), + onClick: () => { + window.location.assign(useCaseURL); + }, + }; + }; + + const button = ( + + + + ); + const panels = [ + { + id: 0, + items: searchWorkspaces.map(workspaceToItem), + }, + ]; + + return ( + + + + + + + + +

{useCaseTitle} Workspaces

+
+
+
+ + setSearchValue(e.target.value)} + fullWidth + /> +
+ +
+ ); +}; diff --git a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx index d3578498c858..68ed1c67359f 100644 --- a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx +++ b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.test.tsx @@ -109,18 +109,6 @@ describe('', () => { expect(screen.getByText('Observability')).toBeInTheDocument(); }); - it('should close the workspace dropdown list', async () => { - render(); - - fireEvent.click(screen.getByTestId('workspace-select-button')); - - expect(screen.getByText(/all workspaces/i)).toBeInTheDocument(); - fireEvent.click(screen.getByTestId('workspace-select-button')); - await waitFor(() => { - expect(screen.queryByText(/all workspaces/i)).not.toBeInTheDocument(); - }); - }); - it('should navigate to the workspace', () => { coreStartMock.workspaces.workspaceList$.next([ { id: 'workspace-1', name: 'workspace 1', features: ['use-case-observability'] }, diff --git a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx index bda11fb3d113..77d1cd6e602e 100644 --- a/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx +++ b/src/plugins/workspace/public/components/workspace_menu/workspace_menu.tsx @@ -44,7 +44,7 @@ const allWorkspacesTitle = i18n.translate('workspace.menu.title.allWorkspaces', }); const recentWorkspacesTitle = i18n.translate('workspace.menu.title.recentWorkspaces', { - defaultMessage: 'recent workspaces', + defaultMessage: 'Recent workspaces', }); const createWorkspaceButton = i18n.translate('workspace.menu.button.createWorkspace', { @@ -158,6 +158,7 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => { } onClick={() => { + closePopover(); window.location.assign(useCaseURL); }} /> @@ -221,6 +222,7 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => { { + closePopover(); navigateToWorkspaceDetail(coreStart, currentWorkspace.id); }} > @@ -240,6 +242,7 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => { { + closePopover(); coreStart.application.navigateToApp(WORKSPACE_LIST_APP_ID); }} > @@ -251,8 +254,9 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => { - {getWorkspaceListGroup(filteredRecentWorkspaces, 'recent')} - {getWorkspaceListGroup(filteredWorkspaceList, 'all')} + {filteredRecentWorkspaces.length > 0 && + getWorkspaceListGroup(filteredRecentWorkspaces, 'recent')} + {filteredWorkspaceList.length > 0 && getWorkspaceListGroup(filteredWorkspaceList, 'all')} @@ -263,6 +267,7 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => { key={WORKSPACE_LIST_APP_ID} data-test-subj="workspace-menu-view-all-button" onClick={() => { + closePopover(); coreStart.application.navigateToApp(WORKSPACE_LIST_APP_ID); }} > @@ -278,6 +283,7 @@ export const WorkspaceMenu = ({ coreStart, registeredUseCases$ }: Props) => { key={WORKSPACE_CREATE_APP_ID} data-test-subj="workspace-menu-create-workspace-button" onClick={() => { + closePopover(); coreStart.application.navigateToApp(WORKSPACE_CREATE_APP_ID); }} > diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts index b2ed55c08de6..6ffae95d40c3 100644 --- a/src/plugins/workspace/public/plugin.test.ts +++ b/src/plugins/workspace/public/plugin.test.ts @@ -19,9 +19,13 @@ import { savedObjectsManagementPluginMock } from '../../saved_objects_management import { managementPluginMock } from '../../management/public/mocks'; import { UseCaseService } from './services/use_case_service'; import { workspaceClientMock, WorkspaceClientMock } from './workspace_client.mock'; -import { WorkspacePlugin } from './plugin'; +import { WorkspacePlugin, WorkspacePluginStartDeps } from './plugin'; +import { contentManagementPluginMocks } from '../../content_management/public'; describe('Workspace plugin', () => { + const mockDependencies: WorkspacePluginStartDeps = { + contentManagement: contentManagementPluginMocks.createStartContract(), + }; const getSetupMock = () => ({ ...coreMock.createSetup(), chrome: chromeServiceMock.createSetupContract(), @@ -48,7 +52,7 @@ describe('Workspace plugin', () => { const setupMock = getSetupMock(); const coreStart = coreMock.createStart(); await workspacePlugin.setup(setupMock, {}); - workspacePlugin.start(coreStart); + workspacePlugin.start(coreStart, mockDependencies); coreStart.workspaces.currentWorkspaceId$.next('foo'); expect(coreStart.savedObjects.client.setCurrentWorkspace).toHaveBeenCalledWith('foo'); expect(setupMock.application.register).toBeCalledTimes(4); @@ -182,7 +186,7 @@ describe('Workspace plugin', () => { const breadcrumbs = new BehaviorSubject([{ text: 'dashboards' }]); startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs); const workspacePlugin = new WorkspacePlugin(); - workspacePlugin.start(startMock); + workspacePlugin.start(startMock, mockDependencies); expect(startMock.chrome.setBreadcrumbs).toBeCalledWith( expect.arrayContaining([ expect.objectContaining({ @@ -208,7 +212,7 @@ describe('Workspace plugin', () => { ]); startMock.chrome.getBreadcrumbs$.mockReturnValue(breadcrumbs); const workspacePlugin = new WorkspacePlugin(); - workspacePlugin.start(startMock); + workspacePlugin.start(startMock, mockDependencies); expect(startMock.chrome.setBreadcrumbs).not.toHaveBeenCalled(); }); @@ -225,7 +229,7 @@ describe('Workspace plugin', () => { jest.spyOn(navGroupUpdater$, 'next'); expect(navGroupUpdater$.next).not.toHaveBeenCalled(); - workspacePlugin.start(coreStart); + workspacePlugin.start(coreStart, mockDependencies); waitFor(() => { expect(navGroupUpdater$.next).toHaveBeenCalled(); @@ -236,7 +240,7 @@ describe('Workspace plugin', () => { const coreStart = coreMock.createStart(); coreStart.chrome.navGroup.getNavGroupEnabled.mockReturnValue(true); const workspacePlugin = new WorkspacePlugin(); - workspacePlugin.start(coreStart); + workspacePlugin.start(coreStart, mockDependencies); expect(coreStart.chrome.navControls.registerLeftBottom).toBeCalledTimes(1); }); @@ -265,7 +269,7 @@ describe('Workspace plugin', () => { const appUpdater$ = setupMock.application.registerAppUpdater.mock.calls[0][0]; - workspacePlugin.start(coreStart); + workspacePlugin.start(coreStart, mockDependencies); const appUpdater = await appUpdater$.pipe(first()).toPromise(); @@ -286,7 +290,7 @@ describe('Workspace plugin', () => { const navGroupUpdater$ = setupMock.chrome.navGroup.registerNavGroupUpdater.mock.calls[0][0]; - workspacePlugin.start(coreStart); + workspacePlugin.start(coreStart, mockDependencies); const navGroupUpdater = await navGroupUpdater$.pipe(first()).toPromise(); @@ -337,7 +341,7 @@ describe('Workspace plugin', () => { const appUpdaterChangeMock = jest.fn(); appUpdater$.subscribe(appUpdaterChangeMock); - workspacePlugin.start(coreStart); + workspacePlugin.start(coreStart, mockDependencies); // Wait for filterNav been executed await new Promise(setImmediate); diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 104db7d9b91f..40f475d4a818 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -7,6 +7,7 @@ import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; import React from 'react'; import { i18n } from '@osd/i18n'; import { map } from 'rxjs/operators'; +import { EuiIcon } from '@elastic/eui'; import { Plugin, CoreStart, @@ -28,6 +29,7 @@ import { WORKSPACE_DETAIL_APP_ID, WORKSPACE_CREATE_APP_ID, WORKSPACE_LIST_APP_ID, + WORKSPACE_USE_CASES, } from '../common/constants'; import { getWorkspaceIdFromUrl } from '../../../core/public/utils'; import { Services, WorkspaceUseCase } from './types'; @@ -46,6 +48,9 @@ import { import { recentWorkspaceManager } from './recent_workspace_manager'; import { toMountPoint } from '../../opensearch_dashboards_react/public'; import { UseCaseService } from './services/use_case_service'; +import { ContentManagementPluginStart } from '../../../plugins/content_management/public'; +import { UseCaseFooter } from './components/home_get_start_card'; +import { HOME_CONTENT_AREAS } from '../../home/public'; type WorkspaceAppType = ( params: AppMountParameters, @@ -59,7 +64,12 @@ interface WorkspacePluginSetupDeps { dataSourceManagement?: DataSourceManagementPluginSetup; } -export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> { +export interface WorkspacePluginStartDeps { + contentManagement: ContentManagementPluginStart; +} + +export class WorkspacePlugin + implements Plugin<{}, {}, WorkspacePluginSetupDeps, WorkspacePluginStartDeps> { private coreStart?: CoreStart; private currentWorkspaceSubscription?: Subscription; private breadcrumbsSubscription?: Subscription; @@ -372,7 +382,41 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> return {}; } - public start(core: CoreStart) { + private registerGetStartedCardToNewHome( + core: CoreStart, + contentManagement: ContentManagementPluginStart + ) { + const useCases = [ + WORKSPACE_USE_CASES.observability, + WORKSPACE_USE_CASES['security-analytics'], + WORKSPACE_USE_CASES.search, + WORKSPACE_USE_CASES.analytics, + ]; + + useCases.forEach((useCase, index) => { + contentManagement.registerContentProvider({ + id: `home_get_start_${useCase.id}`, + getTargetArea: () => HOME_CONTENT_AREAS.GET_STARTED, + getContent: () => ({ + id: useCase.id, + kind: 'card', + order: (index + 1) * 1000, + description: useCase.description, + title: useCase.title, + getIcon: () => React.createElement(EuiIcon, { size: 'xl', type: 'logoOpenSearch' }), + getFooter: () => + React.createElement(UseCaseFooter, { + useCaseId: useCase.id, + useCaseTitle: useCase.title, + core, + registeredUseCases$: this.registeredUseCases$, + }), + }), + }); + }); + } + + public start(core: CoreStart, { contentManagement }: WorkspacePluginStartDeps) { this.coreStart = core; this.currentWorkspaceIdSubscription = this._changeSavedObjectCurrentWorkspace(); @@ -405,8 +449,10 @@ export class WorkspacePlugin implements Plugin<{}, {}, WorkspacePluginSetupDeps> }) ), }); - } + // register get started card in new home page + this.registerGetStartedCardToNewHome(core, contentManagement); + } return {}; }