From 69c9e7f7a092a8c32ff0e05626aceee120c634dd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 3 Sep 2024 03:08:02 +0000 Subject: [PATCH] [Workspace] Refactor get start card at new home page (#7920) * refactor started card Signed-off-by: yubonluo * refactor new home page get start card Signed-off-by: yubonluo * Changeset file for PR #7920 created/updated * revert code 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> (cherry picked from commit d13007a949c832c3914a4ac1db5dfb1b9ac1fba3) Signed-off-by: github-actions[bot] --- changelogs/fragments/7920.yml | 2 + .../card_container/card_embeddable.tsx | 12 +- .../public/components/card_container/types.ts | 4 +- .../public/components/section_input.ts | 4 +- .../public/components/section_render.tsx | 11 +- .../services/content_management/types.ts | 4 +- .../components/home_get_start_card/index.ts | 3 +- .../setup_get_start_card.test.tsx | 198 +++++++++++++++ .../setup_get_start_card.tsx | 76 ++++++ .../use_case_card_title.test.tsx | 122 ++++++++++ .../use_case_card_title.tsx | 176 ++++++++++++++ .../use_case_footer.test.tsx | 149 ------------ .../home_get_start_card/use_case_footer.tsx | 226 ------------------ src/plugins/workspace/public/plugin.ts | 41 +--- 14 files changed, 601 insertions(+), 427 deletions(-) create mode 100644 changelogs/fragments/7920.yml create mode 100644 src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.test.tsx create mode 100644 src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.tsx create mode 100644 src/plugins/workspace/public/components/home_get_start_card/use_case_card_title.test.tsx create mode 100644 src/plugins/workspace/public/components/home_get_start_card/use_case_card_title.tsx delete mode 100644 src/plugins/workspace/public/components/home_get_start_card/use_case_footer.test.tsx delete mode 100644 src/plugins/workspace/public/components/home_get_start_card/use_case_footer.tsx diff --git a/changelogs/fragments/7920.yml b/changelogs/fragments/7920.yml new file mode 100644 index 000000000000..78e677d947fe --- /dev/null +++ b/changelogs/fragments/7920.yml @@ -0,0 +1,2 @@ +refactor: +- [Workspace] Refactor get start card at new home page ([#7920](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7920)) \ No newline at end of file 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 1631b9e13959..5708c2c7c4f7 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,13 +5,15 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { EuiCard, EuiCardProps } from '@elastic/eui'; +import { EuiCard, EuiCardProps, EuiToolTip } from '@elastic/eui'; import { Embeddable, EmbeddableInput, IContainer } from '../../../../embeddable/public'; export const CARD_EMBEDDABLE = 'card_embeddable'; export type CardEmbeddableInput = EmbeddableInput & { description: string; + toolTipContent?: string; + getTitle?: () => React.ReactElement; onClick?: () => void; getIcon?: () => React.ReactElement; getFooter?: () => React.ReactElement; @@ -34,8 +36,12 @@ export class CardEmbeddable extends Embeddable { const cardProps: EuiCardProps = { ...this.input.cardProps, - title: this.input.title ?? '', - description: this.input.description, + title: (this.input?.getTitle?.() || this.input?.title) ?? '', + description: ( + + <>{this.input.description} + + ), onClick: this.input.onClick, icon: this.input?.getIcon?.(), }; 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 4ddaf132ca93..17a3363faf02 100644 --- a/src/plugins/content_management/public/components/card_container/types.ts +++ b/src/plugins/content_management/public/components/card_container/types.ts @@ -7,8 +7,10 @@ import { EuiCardProps } from '@elastic/eui'; import { ContainerInput } from '../../../../embeddable/public'; export interface CardExplicitInput { - title: string; + title?: string; description: string; + toolTipContent?: string; + getTitle?: () => React.ReactElement; onClick?: () => void; getIcon?: () => React.ReactElement; getFooter?: () => React.ReactElement; diff --git a/src/plugins/content_management/public/components/section_input.ts b/src/plugins/content_management/public/components/section_input.ts index 1404e7cd912b..c9049d4baf92 100644 --- a/src/plugins/content_management/public/components/section_input.ts +++ b/src/plugins/content_management/public/components/section_input.ts @@ -45,8 +45,10 @@ export const createCardInput = ( type: CARD_EMBEDDABLE, explicitInput: { id: content.id, - title: content.title, + title: content?.title, description: content.description, + toolTipContent: content?.toolTipContent, + getTitle: content?.getTitle, 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 9f7ddaf6e2f6..746dbd92bb16 100644 --- a/src/plugins/content_management/public/components/section_render.tsx +++ b/src/plugins/content_management/public/components/section_render.tsx @@ -6,7 +6,7 @@ import React, { useState, useEffect, useMemo } from 'react'; import { useObservable } from 'react-use'; import { BehaviorSubject } from 'rxjs'; -import { EuiButtonIcon, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiButtonIcon, EuiSpacer, EuiTitle } from '@elastic/eui'; import { SavedObjectsClientContract } from 'opensearch-dashboards/public'; import { Content, Section } from '../services'; import { EmbeddableInput, EmbeddableRenderer, EmbeddableStart } from '../../../embeddable/public'; @@ -54,9 +54,6 @@ 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 ?? []); @@ -66,13 +63,13 @@ const CardSection = ({ section, embeddable, contents$ }: Props) => { if (section.kind === 'card' && factory && input) { return ( - + <> {section.title ? (

setIsCardVisible(!isCardVisible)} color="text" aria-label={isCardVisible ? 'Show panel' : 'Hide panel'} /> @@ -85,7 +82,7 @@ const CardSection = ({ section, embeddable, contents$ }: Props) => { )} - + ); } 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 11253b1138e2..06d001387d80 100644 --- a/src/plugins/content_management/public/services/content_management/types.ts +++ b/src/plugins/content_management/public/services/content_management/types.ts @@ -70,8 +70,10 @@ export type Content = kind: 'card'; id: string; order: number; - title: string; + title?: string; description: string; + toolTipContent?: string; + getTitle?: () => React.ReactElement; onClick?: () => void; getIcon?: () => React.ReactElement; getFooter?: () => React.ReactElement; 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 index f78300a492d3..994fa3eb7a30 100644 --- a/src/plugins/workspace/public/components/home_get_start_card/index.ts +++ b/src/plugins/workspace/public/components/home_get_start_card/index.ts @@ -3,4 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { UseCaseFooter } from './use_case_footer'; +export { UseCaseCardTitle } from './use_case_card_title'; +export { registerGetStartedCardToNewHome } from './setup_get_start_card'; diff --git a/src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.test.tsx b/src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.test.tsx new file mode 100644 index 000000000000..9182ca1fb007 --- /dev/null +++ b/src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.test.tsx @@ -0,0 +1,198 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ContentManagementPluginStart } from '../../../../../plugins/content_management/public'; +import { coreMock } from '../../../../../core/public/mocks'; +import { registerGetStartedCardToNewHome } from './setup_get_start_card'; +import { createMockedRegisteredUseCases$ } from '../../mocks'; +import { WorkspaceObject } from 'opensearch-dashboards/public'; + +describe('Setup use get start card at new home page', () => { + const navigateToApp = jest.fn(); + + const getMockCore = (workspaceList: WorkspaceObject[], isDashboardAdmin: boolean) => { + const coreStartMock = coreMock.createStart(); + coreStartMock.application.capabilities = { + ...coreStartMock.application.capabilities, + dashboards: { isDashboardAdmin }, + }; + coreStartMock.workspaces.workspaceList$.next(workspaceList); + coreStartMock.application = { + ...coreStartMock.application, + navigateToApp, + }; + jest.spyOn(coreStartMock.application, 'getUrlForApp').mockImplementation((appId: string) => { + return `https://test.com/app/${appId}`; + }); + return coreStartMock; + }; + const registerContentProviderMock = jest.fn(); + const registeredUseCases$ = createMockedRegisteredUseCases$(); + const useCasesMock = [ + { + id: 'dataAdministration', + title: 'Data administration', + description: 'Apply policies or security on your data.', + features: [ + { + id: 'data_administration_landing', + title: 'Overview', + }, + ], + systematic: true, + order: 1000, + }, + { + id: 'essentials', + title: 'Essentials', + description: + 'Analyze data to derive insights, identify patterns and trends, and make data-driven decisions.', + features: [ + { + id: 'essentials_overview', + title: 'Overview', + }, + { + id: 'discover', + title: 'Discover', + }, + ], + systematic: false, + order: 7000, + }, + ]; + registeredUseCases$.next(useCasesMock); + + const contentManagementStartMock: ContentManagementPluginStart = { + registerContentProvider: registerContentProviderMock, + renderPage: jest.fn(), + updatePageSection: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return a tooltip message when there are no workspaces and the user is not dashboard admin', () => { + const core = getMockCore([], false); + registerGetStartedCardToNewHome(core, contentManagementStartMock, registeredUseCases$); + + const calls = registerContentProviderMock.mock.calls; + expect(calls.length).toBe(1); + + const firstCall = calls[0]; + expect(firstCall[0].getTargetArea()).toMatchInlineSnapshot(` + Array [ + "osd_homepage/get_started", + ] + `); + expect(firstCall[0].getContent()).toMatchInlineSnapshot(` + Object { + "cardProps": Object { + "layout": "horizontal", + }, + "description": "Analyze data to derive insights, identify patterns and trends, and make data-driven decisions.", + "getIcon": [Function], + "id": "essentials", + "kind": "card", + "order": 1000, + "title": "Essentials", + "toolTipContent": "Contact your administrator to create a workspace or to be added to an existing one.", + } + `); + }); + + it('should return a getTitle function when there are no workspaces and the user is dashboard admin', () => { + const core = getMockCore([], true); + registerGetStartedCardToNewHome(core, contentManagementStartMock, registeredUseCases$); + + const calls = registerContentProviderMock.mock.calls; + expect(calls.length).toBe(1); + + const firstCall = calls[0]; + expect(firstCall[0].getTargetArea()).toMatchInlineSnapshot(` + Array [ + "osd_homepage/get_started", + ] + `); + expect(firstCall[0].getContent()).toMatchInlineSnapshot(` + Object { + "cardProps": Object { + "layout": "horizontal", + }, + "description": "Analyze data to derive insights, identify patterns and trends, and make data-driven decisions.", + "getIcon": [Function], + "getTitle": [Function], + "id": "essentials", + "kind": "card", + "order": 1000, + } + `); + }); + + it('should return a getTitle function for multiple workspaces', () => { + const workspaces = [ + { id: 'workspace-1', name: 'workspace 1', features: ['use-case-essentials'] }, + { id: 'workspace-2', name: 'workspace 2', features: ['use-case-essentials'] }, + ]; + const core = getMockCore(workspaces, true); + registerGetStartedCardToNewHome(core, contentManagementStartMock, registeredUseCases$); + + const calls = registerContentProviderMock.mock.calls; + expect(calls.length).toBe(1); + + const firstCall = calls[0]; + expect(firstCall[0].getTargetArea()).toMatchInlineSnapshot(` + Array [ + "osd_homepage/get_started", + ] + `); + expect(firstCall[0].getContent()).toMatchInlineSnapshot(` + Object { + "cardProps": Object { + "layout": "horizontal", + }, + "description": "Analyze data to derive insights, identify patterns and trends, and make data-driven decisions.", + "getIcon": [Function], + "getTitle": [Function], + "id": "essentials", + "kind": "card", + "order": 1000, + } + `); + }); + + it('should return a clickable card when there is one workspace', () => { + const workspaces = [ + { id: 'workspace-1', name: 'workspace 1', features: ['use-case-essentials'] }, + ]; + const core = getMockCore(workspaces, true); + registerGetStartedCardToNewHome(core, contentManagementStartMock, registeredUseCases$); + + const calls = registerContentProviderMock.mock.calls; + expect(calls.length).toBe(1); + + const firstCall = calls[0]; + expect(firstCall[0].getTargetArea()).toMatchInlineSnapshot(` + Array [ + "osd_homepage/get_started", + ] + `); + expect(firstCall[0].getContent()).toMatchInlineSnapshot(` + Object { + "cardProps": Object { + "layout": "horizontal", + }, + "description": "Analyze data to derive insights, identify patterns and trends, and make data-driven decisions.", + "getIcon": [Function], + "id": "essentials", + "kind": "card", + "onClick": [Function], + "order": 1000, + "title": "Essentials", + } + `); + }); +}); diff --git a/src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.tsx b/src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.tsx new file mode 100644 index 000000000000..76cda13a39a7 --- /dev/null +++ b/src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.tsx @@ -0,0 +1,76 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; +import { i18n } from '@osd/i18n'; +import { + ContentManagementPluginStart, + HOME_CONTENT_AREAS, +} from '../../../../content_management/public'; +import { WorkspaceUseCase } from '../../types'; +import { getFirstUseCaseOfFeatureConfigs, getUseCaseUrl } from '../../utils'; +import { UseCaseCardTitle } from './use_case_card_title'; + +const createContentCard = (useCase: WorkspaceUseCase, core: CoreStart) => { + const { workspaces, application, http } = core; + const workspaceList = workspaces.workspaceList$.getValue(); + const isDashboardAdmin = application.capabilities?.dashboards?.isDashboardAdmin; + const filterWorkspaces = workspaceList.filter( + (workspace) => getFirstUseCaseOfFeatureConfigs(workspace?.features || []) === useCase.id + ); + if (filterWorkspaces.length === 0 && !isDashboardAdmin) { + return { + title: useCase.title, + toolTipContent: i18n.translate('workspace.getStartCard.noWorkspace.toolTip', { + defaultMessage: + 'Contact your administrator to create a workspace or to be added to an existing one.', + }), + }; + } else if (filterWorkspaces.length === 1) { + const useCaseUrl = getUseCaseUrl(useCase, filterWorkspaces[0], application, http); + return { + onClick: () => { + application.navigateToUrl(useCaseUrl); + }, + title: useCase.title, + }; + } + return { + getTitle: () => + React.createElement(UseCaseCardTitle, { + filterWorkspaces, + useCase, + core, + }), + }; +}; + +export const registerGetStartedCardToNewHome = ( + core: CoreStart, + contentManagement: ContentManagementPluginStart, + registeredUseCases$: BehaviorSubject +) => { + const availableUseCases = registeredUseCases$.getValue().filter((item) => !item.systematic); + availableUseCases.forEach((useCase, index) => { + const content = createContentCard(useCase, core); + 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, + ...content, + getIcon: () => React.createElement(EuiIcon, { size: 'xl', type: 'logoOpenSearch' }), + cardProps: { + layout: 'horizontal', + }, + }), + }); + }); +}; diff --git a/src/plugins/workspace/public/components/home_get_start_card/use_case_card_title.test.tsx b/src/plugins/workspace/public/components/home_get_start_card/use_case_card_title.test.tsx new file mode 100644 index 000000000000..a50145c86646 --- /dev/null +++ b/src/plugins/workspace/public/components/home_get_start_card/use_case_card_title.test.tsx @@ -0,0 +1,122 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { IntlProvider } from 'react-intl'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { coreMock } from '../../../../../core/public/mocks'; + +import { UseCaseCardTitle, UseCaseCardTitleProps } from './use_case_card_title'; +import { WorkspaceUseCase } from '../../types'; + +describe('UseCaseCardTitle', () => { + const navigateToApp = jest.fn(); + const getMockCore = () => { + const coreStartMock = coreMock.createStart(); + coreStartMock.application.capabilities = { + ...coreStartMock.application.capabilities, + }; + coreStartMock.application = { + ...coreStartMock.application, + navigateToApp, + }; + jest.spyOn(coreStartMock.application, 'getUrlForApp').mockImplementation((appId: string) => { + return `https://test.com/app/${appId}`; + }); + return coreStartMock; + }; + + const useCaseMock: WorkspaceUseCase = { + id: 'essentials', + title: 'Essentials', + description: + 'Analyze data to derive insights, identify patterns and trends, and make data-driven decisions.', + features: [ + { + id: 'essentials_overview', + title: 'Overview', + }, + { + id: 'discover', + title: 'Discover', + }, + ], + systematic: false, + order: 7000, + }; + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + const UseCaseCardTitleComponent = (props: UseCaseCardTitleProps) => { + return ( + + + + ); + }; + it('renders create workspace button when no workspaces within use case exist', () => { + const { getByTestId, getByText } = render( + + ); + + const dropDownButton = getByTestId('workspace.getStartCard.essentials.icon.button'); + expect(dropDownButton).toBeInTheDocument(); + fireEvent.click(dropDownButton); + + expect(getByText('No workspaces available')).toBeInTheDocument(); + + const createWorkspaceButton = getByTestId( + 'workspace.getStartCard.essentials.popover.createWorkspace.button' + ); + expect(createWorkspaceButton).toBeInTheDocument(); + expect(createWorkspaceButton).toHaveAttribute('href', 'https://test.com/app/workspace_create'); + }); + + it('renders select workspace popover when multiple workspaces exist', () => { + const workspaces = [ + { id: 'workspace-1', name: 'workspace 1', features: ['essentials'] }, + { id: 'workspace-2', name: 'workspace 2', features: ['essentials'] }, + ]; + + const originalLocation = window.location; + Object.defineProperty(window, 'location', { + value: { + assign: jest.fn(), + }, + }); + + const { getByTestId, getByText } = render( + + ); + + const dropDownButton = getByTestId('workspace.getStartCard.essentials.icon.button'); + expect(dropDownButton).toBeInTheDocument(); + fireEvent.click(dropDownButton); + + expect(getByText('SELECT WORKSPACE')).toBeInTheDocument(); + expect(getByText('workspace 1')).toBeInTheDocument(); + expect(getByText('workspace 2')).toBeInTheDocument(); + + const inputElement = screen.getByPlaceholderText('Search workspace name'); + 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/essentials_overview' + ); + Object.defineProperty(window, 'location', { + value: originalLocation, + }); + }); +}); diff --git a/src/plugins/workspace/public/components/home_get_start_card/use_case_card_title.tsx b/src/plugins/workspace/public/components/home_get_start_card/use_case_card_title.tsx new file mode 100644 index 000000000000..44d34711fef0 --- /dev/null +++ b/src/plugins/workspace/public/components/home_get_start_card/use_case_card_title.tsx @@ -0,0 +1,176 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiText, + EuiTitle, + EuiPanel, + EuiAvatar, + EuiPopover, + EuiFlexItem, + EuiFlexGroup, + EuiFieldSearch, + EuiContextMenu, + EuiButtonIcon, + EuiSmallButton, + EuiPopoverTitle, +} from '@elastic/eui'; +import React, { useMemo, useState } from 'react'; +import { i18n } from '@osd/i18n'; +import { CoreStart, WorkspaceObject } from '../../../../../core/public'; +import { WorkspaceUseCase } from '../../types'; +import { getUseCaseUrl } from '../../utils'; + +export interface UseCaseCardTitleProps { + filterWorkspaces: WorkspaceObject[]; + useCase: WorkspaceUseCase; + core: CoreStart; +} + +export const UseCaseCardTitle = ({ filterWorkspaces, useCase, core }: UseCaseCardTitleProps) => { + const [isPopoverOpen, setPopover] = useState(false); + const [searchValue, setSearchValue] = useState(''); + const onButtonClick = () => setPopover(!isPopoverOpen); + const closePopover = () => setPopover(false); + + const filteredWorkspaces = useMemo( + () => + filterWorkspaces.filter((workspace) => + workspace.name.toLowerCase().includes(searchValue.toLowerCase()) + ), + [filterWorkspaces, searchValue] + ); + + const useCaseTitle = ( + +

{useCase.title}

+
+ ); + + const iconButton = ( + + ); + + if (filterWorkspaces.length === 0) { + const createButton = ( + + {i18n.translate('workspace.getStartCard.popover.createWorkspace.text', { + defaultMessage: 'Create workspace', + })} + + ); + + return ( + + + {useCaseTitle} + + + + + + + {i18n.translate('workspace.getStartCard.popover.noWorkspace.text', { + defaultMessage: 'No workspaces available', + })} + + + {createButton} + + + + + ); + } + + const workspaceToItem = (workspace: WorkspaceObject) => { + const useCaseUrl = getUseCaseUrl(useCase, workspace, core.application, core.http); + const workspaceName = workspace.name; + + return { + name: ( + + {workspaceName} + + ), + key: workspace.id, + icon: ( + + ), + onClick: () => { + window.location.assign(useCaseUrl); + }, + }; + }; + const panels = [ + { + id: 0, + items: filteredWorkspaces.map(workspaceToItem), + width: 340, + }, + ]; + + return ( + + + {useCaseTitle} + + + + + {i18n.translate('workspace.getStartCard.popover.title.', { + defaultMessage: 'SELECT WORKSPACE', + })} + + + setSearchValue(e.target.value)} + /> + +
+ +
+
+
+
+ ); +}; 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 deleted file mode 100644 index 5fbadff102ca..000000000000 --- a/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.test.tsx +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { IntlProvider } from 'react-intl'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { coreMock } from '../../../../../core/public/mocks'; -import { createMockedRegisteredUseCases$ } from '../../mocks'; - -import { UseCaseFooter as UseCaseFooterComponent, UseCaseFooterProps } from './use_case_footer'; - -describe('UseCaseFooter', () => { - // let coreStartMock: CoreStart; - const navigateToApp = jest.fn(); - const registeredUseCases$ = createMockedRegisteredUseCases$(); - - 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 deleted file mode 100644 index d9bfef12d5ab..000000000000 --- a/src/plugins/workspace/public/components/home_get_start_card/use_case_footer.tsx +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - EuiText, - EuiModal, - EuiTitle, - EuiPanel, - EuiAvatar, - EuiSpacer, - EuiPopover, - EuiFlexItem, - EuiModalBody, - EuiFlexGroup, - EuiFieldSearch, - EuiModalFooter, - EuiModalHeader, - EuiContextMenu, - EuiModalHeaderTitle, - EuiSmallButton, -} from '@elastic/eui'; -import React, { useMemo, useState } from 'react'; -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].id ?? - 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 ( - <> - - {i18n.translate('workspace.useCase.footer.createWorkspace', { - defaultMessage: 'Create workspace', - })} - - {isModalVisible && ( - - - {modalHeaderTitle} - - - - {modalBodyContent} - - - - - {i18n.translate('workspace.useCase.footer.modal.close', { - defaultMessage: 'Close', - })} - - {isDashboardAdmin && ( - - {i18n.translate('workspace.useCase.footer.modal.create', { - defaultMessage: 'Create workspace', - })} - - )} - - - )} - - ); - } - - if (filterWorkspaces.length === 1) { - const useCaseURL = formatUrlWithWorkspaceId( - core.application.getUrlForApp(appId, { absolute: false }), - filterWorkspaces[0].id, - basePath - ); - return ( - - {i18n.translate('workspace.useCase.footer.openWorkspace', { defaultMessage: 'Open' })} - - ); - } - - 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 = ( - - {i18n.translate('workspace.useCase.footer.selectWorkspace', { - defaultMessage: 'Select workspace', - })} - - ); - const panels = [ - { - id: 0, - items: searchWorkspaces.map(workspaceToItem), - }, - ]; - - return ( - - - - - - - - -

{useCaseTitle} Workspaces

-
-
-
- - setSearchValue(e.target.value)} - fullWidth - /> -
- -
- ); -}; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 3e04e61a8404..7ccf43a251ee 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -7,7 +7,7 @@ import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; import React from 'react'; import { i18n } from '@osd/i18n'; import { map } from 'rxjs/operators'; -import { EuiIcon, EuiPanel } from '@elastic/eui'; +import { EuiPanel } from '@elastic/eui'; import { Plugin, CoreStart, @@ -29,7 +29,6 @@ import { WORKSPACE_DETAIL_APP_ID, WORKSPACE_CREATE_APP_ID, WORKSPACE_LIST_APP_ID, - WORKSPACE_USE_CASES, WORKSPACE_INITIAL_APP_ID, WORKSPACE_NAVIGATION_APP_ID, } from '../common/constants'; @@ -59,7 +58,6 @@ import { recentWorkspaceManager } from './recent_workspace_manager'; 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 { NavigationPublicPluginStart } from '../../../plugins/navigation/public'; import { WorkspacePickerContent } from './components/workspace_picker_content/workspace_picker_content'; import { HOME_CONTENT_AREAS } from '../../../plugins/content_management/public'; @@ -71,6 +69,7 @@ import { registerAnalyticsAllOverviewContent, setAnalyticsAllOverviewSection, } from './components/use_case_overview/setup_overview'; +import { registerGetStartedCardToNewHome } from './components/home_get_start_card'; type WorkspaceAppType = ( params: AppMountParameters, @@ -554,40 +553,6 @@ export class WorkspacePlugin return {}; } - private registerGetStartedCardToNewHome( - core: CoreStart, - contentManagement: ContentManagementPluginStart - ) { - const useCases = [ - WORKSPACE_USE_CASES.observability, - WORKSPACE_USE_CASES['security-analytics'], - WORKSPACE_USE_CASES.search, - WORKSPACE_USE_CASES.essentials, - ]; - - 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, navigation }: WorkspacePluginStartDeps) { this.coreStart = core; @@ -626,7 +591,7 @@ export class WorkspacePlugin this.registerWorkspaceListToHome(core, contentManagement); // register get started card in new home page - this.registerGetStartedCardToNewHome(core, contentManagement); + registerGetStartedCardToNewHome(core, contentManagement, this.registeredUseCases$); // set breadcrumbs enricher for workspace this.breadcrumbsSubscription = enrichBreadcrumbsWithWorkspace(core);