From ddba225e749e23dd367a22daa6c9a8f7515649b3 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Wed, 6 Nov 2024 16:17:24 +0800 Subject: [PATCH 1/3] dismiss get started for search overview page Signed-off-by: Hailong Cui --- .../content_management/public/mocks.ts | 1 + src/plugins/home/common/constants.ts | 1 + .../home/public/application/application.tsx | 8 +- .../search_use_case_app.test.tsx | 90 ++++++++++++++- .../usecase_overview/search_use_case_app.tsx | 105 +++++++++++++++++- .../search_use_case_setup.tsx | 19 ++-- src/plugins/home/public/plugin.ts | 5 +- src/plugins/home/server/plugin.test.ts | 6 + src/plugins/home/server/plugin.ts | 3 +- src/plugins/home/server/ui_settings.test.ts | 19 +++- src/plugins/home/server/ui_settings.ts | 14 ++- 11 files changed, 248 insertions(+), 23 deletions(-) diff --git a/src/plugins/content_management/public/mocks.ts b/src/plugins/content_management/public/mocks.ts index 3568d5503fea..c39287a1c3b2 100644 --- a/src/plugins/content_management/public/mocks.ts +++ b/src/plugins/content_management/public/mocks.ts @@ -10,6 +10,7 @@ const createStartContract = (): jest.Mocked => { registerContentProvider: jest.fn(), renderPage: jest.fn(), updatePageSection: jest.fn(), + getPage: jest.fn(), }; }; diff --git a/src/plugins/home/common/constants.ts b/src/plugins/home/common/constants.ts index 2760cc328b5b..094c6a2250d8 100644 --- a/src/plugins/home/common/constants.ts +++ b/src/plugins/home/common/constants.ts @@ -36,3 +36,4 @@ export const USE_NEW_HOME_PAGE = 'home:useNewHomePage'; * The id is used in src/plugins/workspace/public/plugin.ts and please change that accordingly if you change the id here. */ export const IMPORT_SAMPLE_DATA_APP_ID = 'import_sample_data'; +export const SEARCH_WORKSPACE_DISMISS_GET_STARTED = 'searchWorkspace:dismissGetStarted'; diff --git a/src/plugins/home/public/application/application.tsx b/src/plugins/home/public/application/application.tsx index f042957eb975..f6d91d2f5ee5 100644 --- a/src/plugins/home/public/application/application.tsx +++ b/src/plugins/home/public/application/application.tsx @@ -106,11 +106,15 @@ export const renderImportSampleDataApp = async ( export const renderSearchUseCaseOverviewApp = async ( element: HTMLElement, coreStart: CoreStart, - contentManagementStart: ContentManagementPluginStart + contentManagementStart: ContentManagementPluginStart, + navigation: NavigationPublicPluginStart ) => { render( - + , element ); diff --git a/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.test.tsx b/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.test.tsx index 001b9bcb1aba..18de762b34bf 100644 --- a/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.test.tsx +++ b/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.test.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { render } from '@testing-library/react'; +import { act, fireEvent, render } from '@testing-library/react'; import React from 'react'; import { coreMock } from '../../../../../../core/public/mocks'; import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; @@ -12,6 +12,9 @@ import { SearchUseCaseOverviewApp } from './search_use_case_app'; import { ContentManagementPluginStart } from '../../../../../content_management/public'; import { ALL_USE_CASE_ID, SEARCH_USE_CASE_ID } from '../../../../../../core/public'; import { BehaviorSubject } from 'rxjs'; +import { navigationPluginMock } from '../../../../../navigation/public/mocks'; +import { NavigationPublicPluginStart } from '../../../../../navigation/public'; +import { TopNavControlsProps } from '../../../../../navigation/public/top_nav_menu'; describe('', () => { const renderPageMock = jest.fn(); @@ -20,6 +23,15 @@ describe('', () => { ...contentManagementPluginMocks.createStartContract(), renderPage: renderPageMock, }; + const navigationMock = navigationPluginMock.createStartContract(); + const FunctionComponent = (props: TopNavControlsProps) => ( +
+ {props.controls?.map((control, idx) => ( +
{control.renderComponent}
+ ))} +
+ ); + navigationMock.ui.HeaderControl = FunctionComponent; const core = coreMock.createStart(); const currentNavGroupMock = new BehaviorSubject({ id: 'Search' }); const coreStartMocks = { @@ -35,11 +47,12 @@ describe('', () => { function renderSearchUseCaseOverviewApp( contentManagement: ContentManagementPluginStart, + navigation: NavigationPublicPluginStart, services = { ...coreStartMocks } ) { return ( - + ); } @@ -50,10 +63,41 @@ describe('', () => { it('render page normally', () => { currentNavGroupMock.next({ id: SEARCH_USE_CASE_ID }); - const { container } = render(renderSearchUseCaseOverviewApp(mock, coreStartMocks)); + const { container } = render( + renderSearchUseCaseOverviewApp(mock, navigationMock, coreStartMocks) + ); expect(container).toMatchInlineSnapshot(`
+
+
+ +
+
+ +
+
+
+
+
dummy page
`); @@ -64,10 +108,48 @@ describe('', () => { it('set page title correctly in all use case', () => { currentNavGroupMock.next({ id: ALL_USE_CASE_ID }); - render(renderSearchUseCaseOverviewApp(mock, coreStartMocks)); + render(renderSearchUseCaseOverviewApp(mock, navigationMock, coreStartMocks)); expect(coreStartMocks.chrome.setBreadcrumbs).toHaveBeenCalledWith([ { text: 'Search Overview' }, ]); }); + + it('user able to dismiss get started', () => { + const { getByTestId, queryByText } = render( + renderSearchUseCaseOverviewApp(mock, navigationMock, coreStartMocks) + ); + + expect(getByTestId('search-overview-setting-button')).toBeInTheDocument(); + act(() => { + fireEvent.click(getByTestId('search-overview-setting-button')); + }); + expect(queryByText('Hide Get started with Search')).toBeInTheDocument(); + + act(() => { + fireEvent.click(queryByText('Hide Get started with Search')!); + }); + expect(coreStartMocks.uiSettings.set).toBeCalledWith('searchWorkspace:dismissGetStarted', true); + }); + + it('user able to enable get started', () => { + coreStartMocks.uiSettings.get.mockReturnValueOnce(true); + const { getByTestId, queryByText } = render( + renderSearchUseCaseOverviewApp(mock, navigationMock, coreStartMocks) + ); + + expect(getByTestId('search-overview-setting-button')).toBeInTheDocument(); + act(() => { + fireEvent.click(getByTestId('search-overview-setting-button')); + }); + expect(queryByText('Show Get started with Search')).toBeInTheDocument(); + + act(() => { + fireEvent.click(queryByText('Show Get started with Search')!); + }); + expect(coreStartMocks.uiSettings.set).toBeCalledWith( + 'searchWorkspace:dismissGetStarted', + false + ); + }); }); diff --git a/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.tsx b/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.tsx index ca59365ad62b..49e64e2cf701 100644 --- a/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.tsx +++ b/src/plugins/home/public/application/components/usecase_overview/search_use_case_app.tsx @@ -3,31 +3,53 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { useObservable } from 'react-use'; import { CoreStart } from 'opensearch-dashboards/public'; -import { EuiBreadcrumb } from '@elastic/eui'; +import { + EuiBreadcrumb, + EuiButtonIcon, + EuiContextMenu, + EuiIcon, + EuiPopover, + EuiToolTip, +} from '@elastic/eui'; import { I18nProvider } from '@osd/i18n/react'; import { i18n } from '@osd/i18n'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { ContentManagementPluginStart, SEARCH_OVERVIEW_PAGE_ID, + SECTIONS, } from '../../../../../content_management/public'; import { SEARCH_USE_CASE_ID } from '../../../../../../core/public'; +import { NavigationPublicPluginStart } from '../../../../../navigation/public'; +import { getStartedSection } from './search_use_case_setup'; +import { SEARCH_WORKSPACE_DISMISS_GET_STARTED } from '../../../../common/constants'; interface Props { contentManagement: ContentManagementPluginStart; + navigation: NavigationPublicPluginStart; } -export const SearchUseCaseOverviewApp = ({ contentManagement }: Props) => { +export const SearchUseCaseOverviewApp = ({ contentManagement, navigation }: Props) => { const { - services: { chrome }, + services: { chrome, application, uiSettings }, } = useOpenSearchDashboards(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isGetStartedDismissed, setIsGetStartedDismissed] = useState( + !!uiSettings.get(SEARCH_WORKSPACE_DISMISS_GET_STARTED) + ); + + const togglePopover = () => setIsPopoverOpen(!isPopoverOpen); + const closePopover = () => setIsPopoverOpen(false); const currentNavGroup = useObservable(chrome.navGroup.getCurrentNavGroup$()); const isSearchUseCase = currentNavGroup?.id === SEARCH_USE_CASE_ID; + const HeaderControl = navigation.ui.HeaderControl; + const page = contentManagement.getPage(SEARCH_OVERVIEW_PAGE_ID); + useEffect(() => { const title = i18n.translate('home.searchOverview.title', { defaultMessage: 'Overview' }); const titleWithUseCase = i18n.translate('home.searchOverview.titleWithUseCase', { @@ -48,8 +70,83 @@ export const SearchUseCaseOverviewApp = ({ contentManagement }: Props) => { chrome.setBreadcrumbs(breadcrumbs); }, [chrome, isSearchUseCase]); + const dismissGetStartCards = async (state: boolean) => { + uiSettings.set(SEARCH_WORKSPACE_DISMISS_GET_STARTED, state); + setIsGetStartedDismissed(state); + }; + + useEffect(() => { + if (isGetStartedDismissed) { + page?.removeSection(SECTIONS.GET_STARTED); + } else { + page?.createSection(getStartedSection); + } + }, [isGetStartedDismissed, page]); + + const hide = i18n.translate('home.searchOverview.getStartedCard.setting.hide.label', { + defaultMessage: 'Hide Get started with Search', + }); + + const show = i18n.translate('home.searchOverview.getStartedCard.setting.show.label', { + defaultMessage: 'Show Get started with Search', + }); + const contextMenuItems = [ + { + name: isGetStartedDismissed ? show : hide, + icon: , + onClick: async () => { + await dismissGetStartCards(!isGetStartedDismissed); + closePopover(); + }, + }, + ]; + + const settingToolTip = i18n.translate('home.searchOverview.getStartedCard.setting.tooltip', { + defaultMessage: 'Page settings', + }); + + const pageHeaderButton = () => { + const popOver = ( + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + + + ); + return isPopoverOpen ? popOver : {popOver}; + }; + + const TopNavControls = [ + { + renderComponent: pageHeaderButton(), + }, + ]; + return ( + {contentManagement ? contentManagement.renderPage(SEARCH_OVERVIEW_PAGE_ID) : null} ); diff --git a/src/plugins/home/public/application/components/usecase_overview/search_use_case_setup.tsx b/src/plugins/home/public/application/components/usecase_overview/search_use_case_setup.tsx index b8c5102d9ccd..4b09b4a9e80f 100644 --- a/src/plugins/home/public/application/components/usecase_overview/search_use_case_setup.tsx +++ b/src/plugins/home/public/application/components/usecase_overview/search_use_case_setup.tsx @@ -13,23 +13,26 @@ import { SEARCH_OVERVIEW_PAGE_ID, SECTIONS, SEARCH_OVERVIEW_CONTENT_AREAS, + Section, } from '../../../../../content_management/public'; const DISCOVER_APP_ID = 'discover'; +export const getStartedSection: Section = { + id: SECTIONS.GET_STARTED, + order: 1000, + title: i18n.translate('home.searchOverview.setupSection.title', { + defaultMessage: 'Set up search', + }), + kind: 'card', +}; + export const setupSearchUseCase = (contentManagement: ContentManagementPluginSetup) => { contentManagement.registerPage({ id: SEARCH_OVERVIEW_PAGE_ID, title: 'Overview', sections: [ - { - id: SECTIONS.GET_STARTED, - order: 1000, - title: i18n.translate('home.searchOverview.setupSection.title', { - defaultMessage: 'Set up search', - }), - kind: 'card', - }, + getStartedSection, { id: SECTIONS.DIFFERENT_SEARCH_TYPES, order: 2000, diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 435c7d4d3b9f..6d60b0917ab8 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -196,7 +196,7 @@ export class HomePublicPlugin mount: async (params: AppMountParameters) => { const [ coreStart, - { contentManagement: contentManagementStart }, + { contentManagement: contentManagementStart, navigation }, ] = await core.getStartServices(); setCommonService(); @@ -204,7 +204,8 @@ export class HomePublicPlugin return await renderSearchUseCaseOverviewApp( params.element, coreStart, - contentManagementStart + contentManagementStart, + navigation ); }, }); diff --git a/src/plugins/home/server/plugin.test.ts b/src/plugins/home/server/plugin.test.ts index 25416c244b29..2e1204f0d68b 100644 --- a/src/plugins/home/server/plugin.test.ts +++ b/src/plugins/home/server/plugin.test.ts @@ -31,6 +31,7 @@ import { registryForTutorialsMock, registryForSampleDataMock } from './plugin.test.mocks'; import { HomeServerPlugin } from './plugin'; import { coreMock, httpServiceMock } from '../../../core/server/mocks'; +import { SEARCH_WORKSPACE_DISMISS_GET_STARTED } from '../common/constants'; describe('HomeServerPlugin', () => { beforeEach(() => { @@ -80,6 +81,11 @@ describe('HomeServerPlugin', () => { expect.any(Function) ); }); + + test('register ui settings', () => { + new HomeServerPlugin(initContext).setup(mockCoreSetup, {}); + expect(mockCoreSetup.uiSettings.register).toHaveBeenCalledTimes(2); + }); }); describe('start', () => { diff --git a/src/plugins/home/server/plugin.ts b/src/plugins/home/server/plugin.ts index 4d4d6b60b4da..70cfcd8aae02 100644 --- a/src/plugins/home/server/plugin.ts +++ b/src/plugins/home/server/plugin.ts @@ -41,7 +41,7 @@ import { UsageCollectionSetup } from '../../usage_collection/server'; import { capabilitiesProvider } from './capabilities_provider'; import { sampleDataTelemetry, homepageSavedObjectType } from './saved_objects'; import { registerRoutes } from './routes'; -import { uiSettings } from './ui_settings'; +import { uiSettings, searchOverviewPageUISetting } from './ui_settings'; interface HomeServerPluginSetupDependencies { usageCollection?: UsageCollectionSetup; @@ -60,6 +60,7 @@ export class HomeServerPlugin implements Plugin { const getValidationFn = (setting: UiSettingsParams) => (value: any) => @@ -30,4 +30,21 @@ describe('home settings', () => { ); }); }); + + describe('searchWorkspace:dismissGetStarted', () => { + const validate = getValidationFn( + searchOverviewPageUISetting['searchWorkspace:dismissGetStarted'] + ); + + it('should only accept boolean values', () => { + expect(() => validate(true)).not.toThrow(); + expect(() => validate(false)).not.toThrow(); + expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [string]"` + ); + expect(() => validate(12)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [boolean] but got [number]"` + ); + }); + }); }); diff --git a/src/plugins/home/server/ui_settings.ts b/src/plugins/home/server/ui_settings.ts index ff0fb0ab1420..2c04379cded4 100644 --- a/src/plugins/home/server/ui_settings.ts +++ b/src/plugins/home/server/ui_settings.ts @@ -13,7 +13,8 @@ import { i18n } from '@osd/i18n'; import { schema } from '@osd/config-schema'; import { UiSettingsParams } from 'opensearch-dashboards/server'; -import { USE_NEW_HOME_PAGE } from '../common/constants'; +import { UiSettingScope } from '../../../core/server'; +import { SEARCH_WORKSPACE_DISMISS_GET_STARTED, USE_NEW_HOME_PAGE } from '../common/constants'; export const uiSettings: Record = { [USE_NEW_HOME_PAGE]: { @@ -28,3 +29,14 @@ export const uiSettings: Record = { requiresPageReload: true, }, }; + +export const searchOverviewPageUISetting: Record = { + [SEARCH_WORKSPACE_DISMISS_GET_STARTED]: { + value: false, + description: i18n.translate('home.ui_settings.searchOverview.dismissGetStarted.description', { + defaultMessage: 'Dismiss get started section on search overview page', + }), + scope: UiSettingScope.USER, + schema: schema.boolean(), + }, +}; From 0ecd000bcbffabd60e311c5f809aa10101428dbc Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 06:25:19 +0000 Subject: [PATCH 2/3] Changeset file for PR #8874 created/updated --- changelogs/fragments/8874.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/8874.yml diff --git a/changelogs/fragments/8874.yml b/changelogs/fragments/8874.yml new file mode 100644 index 000000000000..739f9d4251c4 --- /dev/null +++ b/changelogs/fragments/8874.yml @@ -0,0 +1,2 @@ +feat: +- [Workspace] support dismiss get started for search overview page ([#8874](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8874)) \ No newline at end of file From 8b27d2d477d24194daadc604f2b657d981f96692 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Fri, 29 Nov 2024 15:40:38 +0800 Subject: [PATCH 3/3] support dismiss getting start for essential/analytics overview Signed-off-by: Hailong Cui --- src/plugins/workspace/common/constants.ts | 2 + .../use_case_overview/setup_overview.tsx | 19 +- .../workspace_use_case_overview_app.test.tsx | 197 ++++++++++++++++++ .../workspace_use_case_overview_app.tsx | 138 +++++++++++- src/plugins/workspace/public/plugin.ts | 6 +- src/plugins/workspace/server/ui_settings.ts | 29 ++- 6 files changed, 373 insertions(+), 18 deletions(-) create mode 100644 src/plugins/workspace/public/components/workspace_use_case_overview_app.test.tsx diff --git a/src/plugins/workspace/common/constants.ts b/src/plugins/workspace/common/constants.ts index 7ea882c03c14..9567c364a701 100644 --- a/src/plugins/workspace/common/constants.ts +++ b/src/plugins/workspace/common/constants.ts @@ -24,6 +24,8 @@ export const WORKSPACE_UI_SETTINGS_CLIENT_WRAPPER_ID = 'workspace_ui_settings'; * UI setting for user default workspace */ export const DEFAULT_WORKSPACE = 'defaultWorkspace'; +export const ESSENTIAL_WORKSPACE_DISMISS_GET_STARTED = 'essentialWorkspace:dismissGetStarted'; +export const ANALYTICS_WORKSPACE_DISMISS_GET_STARTED = 'analyticsWorkspace:dismissGetStarted'; export enum WorkspacePermissionMode { Read = 'read', diff --git a/src/plugins/workspace/public/components/use_case_overview/setup_overview.tsx b/src/plugins/workspace/public/components/use_case_overview/setup_overview.tsx index 4cb4859c40e4..e020903039ad 100644 --- a/src/plugins/workspace/public/components/use_case_overview/setup_overview.tsx +++ b/src/plugins/workspace/public/components/use_case_overview/setup_overview.tsx @@ -15,6 +15,7 @@ import { ESSENTIAL_OVERVIEW_CONTENT_AREAS, ESSENTIAL_OVERVIEW_PAGE_ID, SECTIONS, + Section, } from '../../../../content_management/public'; import { getStartedCards } from './get_started_cards'; import { DEFAULT_NAV_GROUPS } from '../../../../../core/public'; @@ -35,6 +36,12 @@ const recentlyViewSectionRender = (contents: Content[]) => { ); }; +export const getStartedSection: Section = { + id: SECTIONS.GET_STARTED, + order: 1000, + kind: 'card', +}; + // Essential overview part export const setEssentialOverviewSection = (contentManagement: ContentManagementPluginSetup) => { contentManagement.registerPage({ @@ -53,11 +60,7 @@ export const setEssentialOverviewSection = (contentManagement: ContentManagement kind: 'custom', render: recentlyViewSectionRender, }, - { - id: SECTIONS.GET_STARTED, - order: 1000, - kind: 'card', - }, + getStartedSection, ], }); }; @@ -107,11 +110,7 @@ export const setAnalyticsAllOverviewSection = (contentManagement: ContentManagem kind: 'custom', render: recentlyViewSectionRender, }, - { - id: SECTIONS.GET_STARTED, - order: 1000, - kind: 'card', - }, + getStartedSection, ], }); }; diff --git a/src/plugins/workspace/public/components/workspace_use_case_overview_app.test.tsx b/src/plugins/workspace/public/components/workspace_use_case_overview_app.test.tsx new file mode 100644 index 000000000000..f7f8a9fa1f03 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_use_case_overview_app.test.tsx @@ -0,0 +1,197 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { act, fireEvent, render, waitFor, screen } from '@testing-library/react'; +import { WorkspaceUseCaseOverviewApp } from './workspace_use_case_overview_app'; +import { of } from 'rxjs'; +import { coreMock } from '../../../../core/public/mocks'; +import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; +import { + ANALYTICS_ALL_OVERVIEW_PAGE_ID, + contentManagementPluginMocks, + ESSENTIAL_OVERVIEW_PAGE_ID, +} from '../../../content_management/public'; +import { + ANALYTICS_WORKSPACE_DISMISS_GET_STARTED, + ESSENTIAL_WORKSPACE_DISMISS_GET_STARTED, +} from '../../common/constants'; +import { navigationPluginMock } from '../../../navigation/public/mocks'; +import { TopNavControlsProps } from '../../../navigation/public/top_nav_menu'; + +const coreStartMocks = coreMock.createStart(); +const navigationMock = navigationPluginMock.createStartContract(); +const FunctionComponent = (props: TopNavControlsProps) => ( +
+ {props.controls?.map((control: any, idx) => ( +
{control.renderComponent}
+ ))} +
+); +navigationMock.ui.HeaderControl = FunctionComponent; + +const contentManagementMock = { + ...contentManagementPluginMocks.createStartContract(), + renderPage: jest.fn(() =>
Mocked Page Content
), +}; + +const servicesMock = { + ...coreStartMocks, + navigationUI: navigationMock.ui, + contentManagement: contentManagementMock, + collaboratorTypes: { + getTypes$: () => of([]), + }, +}; + +function overviewPage(pageId = ESSENTIAL_OVERVIEW_PAGE_ID) { + return ( + + + + ); +} + +describe('WorkspaceUseCaseOverviewApp - Essential', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders without crashing', () => { + const { container } = render(overviewPage()); + expect(container).toMatchInlineSnapshot(` +
+
+
+ +
+
+ +
+
+
+
+
+
+ Mocked Page Content +
+
+ `); + }); + + it('sets breadcrumbs on mount', () => { + render(overviewPage()); + expect(coreStartMocks.chrome.setBreadcrumbs).toHaveBeenCalledWith([{ text: 'Overview' }]); + }); + + it('renders header control when available', () => { + const { getByTestId } = render(overviewPage()); + // setting button exists + expect(getByTestId('essentials_overview-setting-button')).toBeInTheDocument(); + }); + + it('handles get started card dismissal', async () => { + const { getByTestId, getByText } = render(overviewPage()); + + const settingButton = getByTestId(`${ESSENTIAL_OVERVIEW_PAGE_ID}-setting-button`); + expect(settingButton).toBeInTheDocument(); + act(() => { + fireEvent.click(settingButton); + }); + + const hideButton = getByText(/Hide Get started with/i); + expect(hideButton).toBeInTheDocument(); + act(() => { + fireEvent.click(hideButton); + }); + + await waitFor(() => { + expect(coreStartMocks.uiSettings.set).toHaveBeenCalledWith( + ESSENTIAL_WORKSPACE_DISMISS_GET_STARTED, + true + ); + }); + }); + + it('checks initial get started dismissed state', () => { + coreStartMocks.uiSettings.get.mockReturnValueOnce(true); + + const { queryByText, getByTestId } = render(overviewPage()); + + expect(coreStartMocks.uiSettings.get).toHaveBeenCalledWith( + ESSENTIAL_WORKSPACE_DISMISS_GET_STARTED + ); + + const settingButton = getByTestId(`${ESSENTIAL_OVERVIEW_PAGE_ID}-setting-button`); + expect(settingButton).toBeInTheDocument(); + act(() => { + fireEvent.click(settingButton); + }); + + expect(queryByText(/Show Get started/i)).toBeInTheDocument(); + + act(() => { + fireEvent.click(queryByText(/Show Get started/)!); + }); + expect(coreStartMocks.uiSettings.set).toBeCalledWith( + ESSENTIAL_WORKSPACE_DISMISS_GET_STARTED, + false + ); + }); + + it('renders content management page when available', () => { + const { queryByText } = render(overviewPage()); + + expect(contentManagementMock.renderPage).toHaveBeenCalledWith(ESSENTIAL_OVERVIEW_PAGE_ID); + expect(queryByText('Mocked Page Content')).toBeInTheDocument(); + }); +}); + +describe('WorkspaceUseCaseOverviewApp - Analytics', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('handles get started card dismissal', async () => { + const { getByTestId, getByText } = render(overviewPage(ANALYTICS_ALL_OVERVIEW_PAGE_ID)); + + const settingButton = getByTestId(`${ANALYTICS_ALL_OVERVIEW_PAGE_ID}-setting-button`); + expect(settingButton).toBeInTheDocument(); + act(() => { + fireEvent.click(settingButton); + }); + + const hideButton = getByText('Hide Get started with Analytics'); + expect(hideButton).toBeInTheDocument(); + act(() => { + fireEvent.click(hideButton); + }); + + await waitFor(() => { + expect(coreStartMocks.uiSettings.set).toHaveBeenCalledWith( + ANALYTICS_WORKSPACE_DISMISS_GET_STARTED, + true + ); + }); + }); +}); diff --git a/src/plugins/workspace/public/components/workspace_use_case_overview_app.tsx b/src/plugins/workspace/public/components/workspace_use_case_overview_app.tsx index c044716eeb0b..442f66ffd16d 100644 --- a/src/plugins/workspace/public/components/workspace_use_case_overview_app.tsx +++ b/src/plugins/workspace/public/components/workspace_use_case_overview_app.tsx @@ -3,13 +3,31 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { I18nProvider } from '@osd/i18n/react'; import { i18n } from '@osd/i18n'; import { useObservable } from 'react-use'; -import { EuiBreadcrumb } from '@elastic/eui'; +import { + EuiBreadcrumb, + EuiButtonIcon, + EuiContextMenu, + EuiIcon, + EuiPopover, + EuiToolTip, +} from '@elastic/eui'; import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; import { Services } from '../types'; +import { + ANALYTICS_WORKSPACE_DISMISS_GET_STARTED, + ESSENTIAL_WORKSPACE_DISMISS_GET_STARTED, +} from '../../common/constants'; +import { getStartedSection } from './use_case_overview/setup_overview'; +import { + ESSENTIAL_OVERVIEW_PAGE_ID, + ANALYTICS_ALL_OVERVIEW_PAGE_ID, + SECTIONS, +} from '../../../content_management/public'; +import { DEFAULT_NAV_GROUPS } from '../../../../core/public'; interface WorkspaceUseCaseOverviewProps { pageId: string; @@ -17,7 +35,7 @@ interface WorkspaceUseCaseOverviewProps { export const WorkspaceUseCaseOverviewApp = (props: WorkspaceUseCaseOverviewProps) => { const { - services: { contentManagement, workspaces, chrome }, + services: { contentManagement, workspaces, chrome, application, uiSettings, navigationUI }, } = useOpenSearchDashboards(); const currentWorkspace = useObservable(workspaces.currentWorkspace$); @@ -31,9 +49,119 @@ export const WorkspaceUseCaseOverviewApp = (props: WorkspaceUseCaseOverviewProps chrome.setBreadcrumbs(breadcrumbs); }, [chrome, currentWorkspace]); - const pageId = props.pageId; + const { pageId } = props; + + const uiSettingsKeyMap: Record = { + [ESSENTIAL_OVERVIEW_PAGE_ID]: ESSENTIAL_WORKSPACE_DISMISS_GET_STARTED, + [ANALYTICS_ALL_OVERVIEW_PAGE_ID]: ANALYTICS_WORKSPACE_DISMISS_GET_STARTED, + }; + + const useCaseNameMap: Record = { + [ESSENTIAL_OVERVIEW_PAGE_ID]: DEFAULT_NAV_GROUPS.essentials.title, + [ANALYTICS_ALL_OVERVIEW_PAGE_ID]: DEFAULT_NAV_GROUPS.all.title, + }; + + const uiSettingsKey = uiSettingsKeyMap[pageId]; + const useCaseName = useCaseNameMap[pageId]; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isGetStartedDismissed, setIsGetStartedDismissed] = useState( + !!uiSettings.get(uiSettingsKey) + ); + + const togglePopover = () => setIsPopoverOpen(!isPopoverOpen); + const closePopover = () => setIsPopoverOpen(false); + + const page = contentManagement?.getPage(pageId); + + const HeaderControl = navigationUI?.HeaderControl; + + const dismissGetStartCards = async (state: boolean) => { + uiSettings.set(uiSettingsKey, state); + setIsGetStartedDismissed(state); + }; + + useEffect(() => { + if (isGetStartedDismissed) { + page?.removeSection(SECTIONS.GET_STARTED); + } else { + page?.createSection(getStartedSection); + } + }, [isGetStartedDismissed, page]); + + const hide = i18n.translate('workspace.overview.getStartedCard.setting.hide.label', { + defaultMessage: 'Hide Get started with {useCaseName}', + values: { + useCaseName, + }, + }); + + const show = i18n.translate('workspace.overview.getStartedCard.setting.show.label', { + defaultMessage: 'Show Get started with {useCaseName}', + values: { + useCaseName, + }, + }); + const contextMenuItems = [ + { + name: isGetStartedDismissed ? show : hide, + icon: , + onClick: async () => { + await dismissGetStartCards(!isGetStartedDismissed); + closePopover(); + }, + }, + ]; + + const settingToolTip = i18n.translate('workspace.overview.getStartedCard.setting.tooltip', { + defaultMessage: 'Page settings', + }); + + const pageHeaderButton = () => { + const popOver = ( + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + + + ); + return isPopoverOpen ? popOver : {popOver}; + }; + + const TopNavControls = [ + { + renderComponent: pageHeaderButton(), + }, + ]; return ( - {contentManagement ? contentManagement.renderPage(pageId) : null} + + {HeaderControl && ( + + )} + {contentManagement ? contentManagement.renderPage(pageId) : null} + ); }; diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 0dbf5fe0bb02..c4b2b1bcdff3 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -476,13 +476,14 @@ export class WorkspacePlugin const { renderUseCaseOverviewApp } = await import('./application'); const [ coreStart, - { contentManagement: contentManagementStart }, + { contentManagement: contentManagementStart, navigation: navigationStart }, ] = await core.getStartServices(); const services = { ...coreStart, workspaceClient, dataSourceManagement, contentManagement: contentManagementStart, + navigationUI: navigationStart.ui, }; return renderUseCaseOverviewApp(params, services, ESSENTIAL_OVERVIEW_PAGE_ID); @@ -511,13 +512,14 @@ export class WorkspacePlugin const { renderUseCaseOverviewApp } = await import('./application'); const [ coreStart, - { contentManagement: contentManagementStart }, + { contentManagement: contentManagementStart, navigation: navigationStart }, ] = await core.getStartServices(); const services = { ...coreStart, workspaceClient, dataSourceManagement, contentManagement: contentManagementStart, + navigationUI: navigationStart.ui, }; return renderUseCaseOverviewApp(params, services, ANALYTICS_ALL_OVERVIEW_PAGE_ID); diff --git a/src/plugins/workspace/server/ui_settings.ts b/src/plugins/workspace/server/ui_settings.ts index a5aceccc0386..c28e6d146b87 100644 --- a/src/plugins/workspace/server/ui_settings.ts +++ b/src/plugins/workspace/server/ui_settings.ts @@ -6,8 +6,13 @@ import { schema } from '@osd/config-schema'; import { UiSettingsParams } from 'opensearch-dashboards/server'; +import { i18n } from '@osd/i18n'; import { UiSettingScope } from '../../../core/server'; -import { DEFAULT_WORKSPACE } from '../common/constants'; +import { + ANALYTICS_WORKSPACE_DISMISS_GET_STARTED, + DEFAULT_WORKSPACE, + ESSENTIAL_WORKSPACE_DISMISS_GET_STARTED, +} from '../common/constants'; export const uiSettings: Record = { [DEFAULT_WORKSPACE]: { @@ -17,4 +22,26 @@ export const uiSettings: Record = { type: 'string', schema: schema.nullable(schema.string()), }, + [ESSENTIAL_WORKSPACE_DISMISS_GET_STARTED]: { + value: false, + description: i18n.translate( + 'workspace.ui_settings.essentialOverview.dismissGetStarted.description', + { + defaultMessage: 'Dismiss get started section on essential overview page', + } + ), + scope: UiSettingScope.USER, + schema: schema.boolean(), + }, + [ANALYTICS_WORKSPACE_DISMISS_GET_STARTED]: { + value: false, + description: i18n.translate( + 'workspace.ui_settings.analyticsOverview.dismissGetStarted.description', + { + defaultMessage: 'Dismiss get started section on analytics overview page', + } + ), + scope: UiSettingScope.USER, + schema: schema.boolean(), + }, };