From 641df309de83307129cba218c4eb1980a4aeeb73 Mon Sep 17 00:00:00 2001 From: yubonluo Date: Wed, 28 Aug 2024 10:58:35 +0800 Subject: [PATCH 1/5] refactor started card Signed-off-by: yubonluo --- .../card_container/card_embeddable.tsx | 40 ++- .../public/components/card_container/types.ts | 5 +- .../public/components/section_input.ts | 5 +- .../public/components/section_render.tsx | 33 ++- .../services/content_management/types.ts | 5 +- .../home/public/application/home_render.tsx | 2 +- .../home_get_start_card/use_case_card.tsx | 229 ++++++++++++++++++ src/plugins/workspace/public/plugin.ts | 88 +++++-- 8 files changed, 364 insertions(+), 43 deletions(-) create mode 100644 src/plugins/workspace/public/components/home_get_start_card/use_case_card.tsx 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..3b46acb9164f 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,16 @@ 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; + showToolTip?: boolean; + toolTipContent?: string; + getTitle?: () => React.ReactElement; onClick?: () => void; getIcon?: () => React.ReactElement; getFooter?: () => React.ReactElement; @@ -34,7 +37,7 @@ export class CardEmbeddable extends Embeddable { const cardProps: EuiCardProps = { ...this.input.cardProps, - title: this.input.title ?? '', + title: (this.input?.getTitle?.() || this.input?.title) ?? '', description: this.input.description, onClick: this.input.onClick, icon: this.input?.getIcon?.(), @@ -45,7 +48,38 @@ export class CardEmbeddable extends Embeddable { cardProps.footer = this.input?.getFooter?.(); } - ReactDOM.render(, node); + const card = ; + + ReactDOM.render( + this.input?.showToolTip ? ( + + {card} + + ) : ( + card + ), + node + ); + // const card = ( + // + // ); + // ReactDOM.render( + // this.input?.showToolTip ? ( + // + // {card} + // + // ) : ( + // card + // ), + // node + // ); } public destroy() { 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..c5e69ab7adf0 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,11 @@ import { EuiCardProps } from '@elastic/eui'; import { ContainerInput } from '../../../../embeddable/public'; export interface CardExplicitInput { - title: string; + title?: string; description: string; + showToolTip?: boolean; + 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 3ae47b84881d..3f7c39521455 100644 --- a/src/plugins/content_management/public/components/section_input.ts +++ b/src/plugins/content_management/public/components/section_input.ts @@ -44,8 +44,11 @@ export const createCardInput = ( type: CARD_EMBEDDABLE, explicitInput: { id: content.id, - title: content.title, + title: content?.title, description: content.description, + showToolTip: content?.showToolTip, + 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 457b07cb7822..7354057944f3 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,26 +63,24 @@ const CardSection = ({ section, embeddable, contents$ }: Props) => { if (section.kind === 'card' && factory && input) { return ( - - {section.title ? ( - -

- - {section.title} -

-
- ) : null} + <> + +

+ setIsCardVisible(!isCardVisible)} + color="text" + aria-label={isCardVisible ? 'Show panel' : 'Hide panel'} + /> + {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 11253b1138e2..d8f1006af36e 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,11 @@ export type Content = kind: 'card'; id: string; order: number; - title: string; + title?: string; description: string; + showToolTip?: boolean; + toolTipContent?: string; + getTitle?: () => React.ReactElement; onClick?: () => void; getIcon?: () => React.ReactElement; getFooter?: () => React.ReactElement; diff --git a/src/plugins/home/public/application/home_render.tsx b/src/plugins/home/public/application/home_render.tsx index 03babca342eb..ef73d7c44d99 100644 --- a/src/plugins/home/public/application/home_render.tsx +++ b/src/plugins/home/public/application/home_render.tsx @@ -50,7 +50,7 @@ export const setupHome = (contentManagement: ContentManagementPluginSetup) => { { id: SECTIONS.GET_STARTED, order: 1000, - title: 'Define your path forward with OpenSearch', + title: 'Get started with OpenSearch’s powerful features', kind: 'card', }, ], diff --git a/src/plugins/workspace/public/components/home_get_start_card/use_case_card.tsx b/src/plugins/workspace/public/components/home_get_start_card/use_case_card.tsx new file mode 100644 index 000000000000..8b313eee5183 --- /dev/null +++ b/src/plugins/workspace/public/components/home_get_start_card/use_case_card.tsx @@ -0,0 +1,229 @@ +/* + * 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 { 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; + + return ( + +

{useCaseTitle}

+
+ ); + + 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..332bf07946fc 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -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'; @@ -52,6 +51,7 @@ import { filterWorkspaceConfigurableApps, getFirstUseCaseOfFeatureConfigs, getUseCaseUrl, + getUseCaseFromFeatureConfig, isAppAccessibleInWorkspace, isNavGroupInFeatureConfigs, } from './utils'; @@ -554,18 +554,67 @@ export class WorkspacePlugin return {}; } + private createContentCard(useCase: WorkspaceUseCase, index: number, core: CoreStart) { + const workspaceList = core.workspaces.workspaceList$.getValue(); + const isDashboardAdmin = core.application.capabilities?.dashboards?.isDashboardAdmin; + const filterWorkspaces = workspaceList.filter( + (workspace) => + workspace.features?.map(getUseCaseFromFeatureConfig).filter(Boolean)[0] === useCase.id + ); + if (filterWorkspaces.length === 0 && !isDashboardAdmin) { + // if (isDashboardAdmin) { + // return { + // onClick: () => { + // core.application.navigateToApp('workspace_create'); + // }, + // title: useCase.title, + // }; + // } else { + // return { + // title: useCase.title, + // showToolTip: true, + // toolTipContent: + // 'Contact your administrator to create a workspace or to be added to an existing one.', + // }; + // } + + return { + title: useCase.title, + showToolTip: true, + toolTipContent: + 'Contact your administrator to create a workspace or to be added to an existing one.', + }; + } + if (filterWorkspaces.length === 1) { + return { + onClick: () => { + core.application.navigateToApp('workspace_create'); + }, + title: useCase.title, + }; + } + return { + getTitle: () => + React.createElement(UseCaseFooter, { + useCaseId: useCase.id, + useCaseTitle: useCase.title, + core, + registeredUseCases$: this.registeredUseCases$, // 假设这是一个 BehaviorSubject 或类似的 + }), + getIcon: () => React.createElement(EuiIcon, { size: 'xl', type: 'logoOpenSearch' }), + }; + } + 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) => { + const availableUseCases = this.registeredUseCases$ + .getValue() + .filter((item) => !item.systematic); + + availableUseCases.forEach((useCase, index) => { + const content = this.createContentCard(useCase, index, core); contentManagement.registerContentProvider({ id: `home_get_start_${useCase.id}`, getTargetArea: () => [HOME_CONTENT_AREAS.GET_STARTED], @@ -574,15 +623,20 @@ export class WorkspacePlugin kind: 'card', order: (index + 1) * 1000, description: useCase.description, - title: useCase.title, + ...content, + // title: 'a', + // getTitle: () => + // React.createElement(UseCaseFooter, { + // useCaseId: useCase.id, + // useCaseTitle: useCase.title, + // core, + // registeredUseCases$: this.registeredUseCases$, + // }), getIcon: () => React.createElement(EuiIcon, { size: 'xl', type: 'logoOpenSearch' }), - getFooter: () => - React.createElement(UseCaseFooter, { - useCaseId: useCase.id, - useCaseTitle: useCase.title, - core, - registeredUseCases$: this.registeredUseCases$, - }), + cardProps: { + layout: 'horizontal', + }, + // onClick: () => {}, }), }); }); From bade6eb045769f2232bbf76cddbeaca22de30f4a Mon Sep 17 00:00:00 2001 From: yubonluo Date: Thu, 29 Aug 2024 23:11:54 +0800 Subject: [PATCH 2/5] refactor new home page get start card Signed-off-by: yubonluo --- .../card_container/card_embeddable.tsx | 40 +-- .../public/components/card_container/types.ts | 1 - .../public/components/section_input.ts | 1 - .../services/content_management/types.ts | 1 - .../components/home_get_start_card/index.ts | 3 +- .../setup_get_start_card.test.tsx | 198 +++++++++++++++ .../setup_get_start_card.tsx | 77 ++++++ .../home_get_start_card/use_case_card.tsx | 229 ------------------ .../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 | 95 +------- 13 files changed, 584 insertions(+), 734 deletions(-) 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 delete mode 100644 src/plugins/workspace/public/components/home_get_start_card/use_case_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/src/plugins/content_management/public/components/card_container/card_embeddable.tsx b/src/plugins/content_management/public/components/card_container/card_embeddable.tsx index 3b46acb9164f..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 @@ -12,7 +12,6 @@ import { Embeddable, EmbeddableInput, IContainer } from '../../../../embeddable/ export const CARD_EMBEDDABLE = 'card_embeddable'; export type CardEmbeddableInput = EmbeddableInput & { description: string; - showToolTip?: boolean; toolTipContent?: string; getTitle?: () => React.ReactElement; onClick?: () => void; @@ -38,7 +37,11 @@ export class CardEmbeddable extends Embeddable { const cardProps: EuiCardProps = { ...this.input.cardProps, title: (this.input?.getTitle?.() || this.input?.title) ?? '', - description: this.input.description, + description: ( + + <>{this.input.description} + + ), onClick: this.input.onClick, icon: this.input?.getIcon?.(), }; @@ -48,38 +51,7 @@ export class CardEmbeddable extends Embeddable { cardProps.footer = this.input?.getFooter?.(); } - const card = ; - - ReactDOM.render( - this.input?.showToolTip ? ( - - {card} - - ) : ( - card - ), - node - ); - // const card = ( - // - // ); - // ReactDOM.render( - // this.input?.showToolTip ? ( - // - // {card} - // - // ) : ( - // card - // ), - // node - // ); + ReactDOM.render(, node); } public destroy() { 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 c5e69ab7adf0..17a3363faf02 100644 --- a/src/plugins/content_management/public/components/card_container/types.ts +++ b/src/plugins/content_management/public/components/card_container/types.ts @@ -9,7 +9,6 @@ import { ContainerInput } from '../../../../embeddable/public'; export interface CardExplicitInput { title?: string; description: string; - showToolTip?: boolean; toolTipContent?: string; getTitle?: () => React.ReactElement; onClick?: () => void; diff --git a/src/plugins/content_management/public/components/section_input.ts b/src/plugins/content_management/public/components/section_input.ts index 79ecdf1a1a01..c9049d4baf92 100644 --- a/src/plugins/content_management/public/components/section_input.ts +++ b/src/plugins/content_management/public/components/section_input.ts @@ -47,7 +47,6 @@ export const createCardInput = ( id: content.id, title: content?.title, description: content.description, - showToolTip: content?.showToolTip, toolTipContent: content?.toolTipContent, getTitle: content?.getTitle, onClick: content.onClick, 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 d8f1006af36e..06d001387d80 100644 --- a/src/plugins/content_management/public/services/content_management/types.ts +++ b/src/plugins/content_management/public/services/content_management/types.ts @@ -72,7 +72,6 @@ export type Content = order: number; title?: string; description: string; - showToolTip?: boolean; toolTipContent?: string; getTitle?: () => React.ReactElement; onClick?: () => void; 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..21cadd34bf69 --- /dev/null +++ b/src/plugins/workspace/public/components/home_get_start_card/setup_get_start_card.tsx @@ -0,0 +1,77 @@ +/* + * 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 { getUseCaseFromFeatureConfig, 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) => + workspace.features?.map(getUseCaseFromFeatureConfig).filter(Boolean)[0] === 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.tsx b/src/plugins/workspace/public/components/home_get_start_card/use_case_card.tsx deleted file mode 100644 index 8b313eee5183..000000000000 --- a/src/plugins/workspace/public/components/home_get_start_card/use_case_card.tsx +++ /dev/null @@ -1,229 +0,0 @@ -/* - * 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 { 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; - - return ( - -

{useCaseTitle}

-
- ); - - 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/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..54cdde1fdedd --- /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 searchWorkspaces = 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: searchWorkspaces.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 332bf07946fc..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, @@ -51,7 +51,6 @@ import { filterWorkspaceConfigurableApps, getFirstUseCaseOfFeatureConfigs, getUseCaseUrl, - getUseCaseFromFeatureConfig, isAppAccessibleInWorkspace, isNavGroupInFeatureConfigs, } from './utils'; @@ -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,94 +553,6 @@ export class WorkspacePlugin return {}; } - private createContentCard(useCase: WorkspaceUseCase, index: number, core: CoreStart) { - const workspaceList = core.workspaces.workspaceList$.getValue(); - const isDashboardAdmin = core.application.capabilities?.dashboards?.isDashboardAdmin; - const filterWorkspaces = workspaceList.filter( - (workspace) => - workspace.features?.map(getUseCaseFromFeatureConfig).filter(Boolean)[0] === useCase.id - ); - if (filterWorkspaces.length === 0 && !isDashboardAdmin) { - // if (isDashboardAdmin) { - // return { - // onClick: () => { - // core.application.navigateToApp('workspace_create'); - // }, - // title: useCase.title, - // }; - // } else { - // return { - // title: useCase.title, - // showToolTip: true, - // toolTipContent: - // 'Contact your administrator to create a workspace or to be added to an existing one.', - // }; - // } - - return { - title: useCase.title, - showToolTip: true, - toolTipContent: - 'Contact your administrator to create a workspace or to be added to an existing one.', - }; - } - if (filterWorkspaces.length === 1) { - return { - onClick: () => { - core.application.navigateToApp('workspace_create'); - }, - title: useCase.title, - }; - } - return { - getTitle: () => - React.createElement(UseCaseFooter, { - useCaseId: useCase.id, - useCaseTitle: useCase.title, - core, - registeredUseCases$: this.registeredUseCases$, // 假设这是一个 BehaviorSubject 或类似的 - }), - getIcon: () => React.createElement(EuiIcon, { size: 'xl', type: 'logoOpenSearch' }), - }; - } - - private registerGetStartedCardToNewHome( - core: CoreStart, - contentManagement: ContentManagementPluginStart - ) { - const availableUseCases = this.registeredUseCases$ - .getValue() - .filter((item) => !item.systematic); - - availableUseCases.forEach((useCase, index) => { - const content = this.createContentCard(useCase, index, 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, - // title: 'a', - // getTitle: () => - // React.createElement(UseCaseFooter, { - // useCaseId: useCase.id, - // useCaseTitle: useCase.title, - // core, - // registeredUseCases$: this.registeredUseCases$, - // }), - getIcon: () => React.createElement(EuiIcon, { size: 'xl', type: 'logoOpenSearch' }), - cardProps: { - layout: 'horizontal', - }, - // onClick: () => {}, - }), - }); - }); - } - public start(core: CoreStart, { contentManagement, navigation }: WorkspacePluginStartDeps) { this.coreStart = core; @@ -680,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); From 81f8534114e8e2e805cd3ef0ed6c29de792f7de8 Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:18:48 +0000 Subject: [PATCH 3/5] Changeset file for PR #7920 created/updated --- changelogs/fragments/7920.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/7920.yml 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 From 571aad62a8f660cf26661b54275ff13450195fc1 Mon Sep 17 00:00:00 2001 From: yubonluo Date: Thu, 29 Aug 2024 23:23:34 +0800 Subject: [PATCH 4/5] revert code Signed-off-by: yubonluo --- .../public/components/section_render.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/plugins/content_management/public/components/section_render.tsx b/src/plugins/content_management/public/components/section_render.tsx index 768e4aab3c50..746dbd92bb16 100644 --- a/src/plugins/content_management/public/components/section_render.tsx +++ b/src/plugins/content_management/public/components/section_render.tsx @@ -64,17 +64,19 @@ const CardSection = ({ section, embeddable, contents$ }: Props) => { if (section.kind === 'card' && factory && input) { return ( <> - -

- setIsCardVisible(!isCardVisible)} - color="text" - aria-label={isCardVisible ? 'Show panel' : 'Hide panel'} - /> - {section.title} -

-
+ {section.title ? ( + +

+ setIsCardVisible(!isCardVisible)} + color="text" + aria-label={isCardVisible ? 'Show panel' : 'Hide panel'} + /> + {section.title} +

+
+ ) : null} {isCardVisible && ( <> From 2f9179d5d410aaf3a7407446d2cf64546f413faa Mon Sep 17 00:00:00 2001 From: yubonluo Date: Fri, 30 Aug 2024 17:35:16 +0800 Subject: [PATCH 5/5] optimize the code Signed-off-by: yubonluo --- .../components/home_get_start_card/setup_get_start_card.tsx | 5 ++--- .../components/home_get_start_card/use_case_card_title.tsx | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) 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 index 21cadd34bf69..76cda13a39a7 100644 --- 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 @@ -12,7 +12,7 @@ import { HOME_CONTENT_AREAS, } from '../../../../content_management/public'; import { WorkspaceUseCase } from '../../types'; -import { getUseCaseFromFeatureConfig, getUseCaseUrl } from '../../utils'; +import { getFirstUseCaseOfFeatureConfigs, getUseCaseUrl } from '../../utils'; import { UseCaseCardTitle } from './use_case_card_title'; const createContentCard = (useCase: WorkspaceUseCase, core: CoreStart) => { @@ -20,8 +20,7 @@ const createContentCard = (useCase: WorkspaceUseCase, core: CoreStart) => { const workspaceList = workspaces.workspaceList$.getValue(); const isDashboardAdmin = application.capabilities?.dashboards?.isDashboardAdmin; const filterWorkspaces = workspaceList.filter( - (workspace) => - workspace.features?.map(getUseCaseFromFeatureConfig).filter(Boolean)[0] === useCase.id + (workspace) => getFirstUseCaseOfFeatureConfigs(workspace?.features || []) === useCase.id ); if (filterWorkspaces.length === 0 && !isDashboardAdmin) { return { 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 index 54cdde1fdedd..44d34711fef0 100644 --- 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 @@ -35,7 +35,7 @@ export const UseCaseCardTitle = ({ filterWorkspaces, useCase, core }: UseCaseCar const onButtonClick = () => setPopover(!isPopoverOpen); const closePopover = () => setPopover(false); - const searchWorkspaces = useMemo( + const filteredWorkspaces = useMemo( () => filterWorkspaces.filter((workspace) => workspace.name.toLowerCase().includes(searchValue.toLowerCase()) @@ -52,7 +52,7 @@ export const UseCaseCardTitle = ({ filterWorkspaces, useCase, core }: UseCaseCar const iconButton = (