From 6ee0d8b9ebf3a2d744b2b62a7eb16ab98e8ff270 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:06:48 +0800 Subject: [PATCH] [Workspace]Refactor use case selector in workspace creation page (#8413) (#8444) * Refactor use case selector in workspace create * Add gap for details panel title * Update use case name and description * Filter out hidden nav links * Changeset file for PR #8413 created/updated * Add test case for different feature details * Change back to "Analytics" * Add all features suffix for all use case * Fix failed unit tests * Filter out getting started links * Renaming isFlyoutVisible to isUseCaseFlyoutVisible --------- (cherry picked from commit 7b5a3797a885943d5b7037fc58dd8c9fb978c7dd) Signed-off-by: Lin Wang Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> --- changelogs/fragments/8413.yml | 2 + src/core/utils/default_nav_groups.ts | 14 +- .../use_case_overview/setup_overview.test.tsx | 2 +- .../creator_details_panel.tsx | 2 + .../public/components/workspace_form/index.ts | 1 + .../public/components/workspace_form/types.ts | 5 +- .../workspace_use_case.test.tsx | 58 +++--- .../workspace_form/workspace_use_case.tsx | 189 +++++++++--------- .../workspace_use_case_flyout.test.tsx | 81 ++++++++ .../workspace_use_case_flyout.tsx | 128 ++++++++++++ .../default_workspace.test.tsx.snap | 2 +- .../__snapshots__/index.test.tsx.snap | 2 +- .../workspace_list/default_workspace.test.tsx | 2 +- .../components/workspace_list/index.test.tsx | 2 +- .../public/services/use_case_service.test.ts | 23 +++ .../public/services/use_case_service.ts | 35 ++-- src/plugins/workspace/public/types.ts | 1 + src/plugins/workspace/public/utils.test.ts | 180 +++++++++++++++-- src/plugins/workspace/public/utils.ts | 70 +++++-- 19 files changed, 612 insertions(+), 187 deletions(-) create mode 100644 changelogs/fragments/8413.yml create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.test.tsx create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.tsx diff --git a/changelogs/fragments/8413.yml b/changelogs/fragments/8413.yml new file mode 100644 index 000000000000..b23b994010e4 --- /dev/null +++ b/changelogs/fragments/8413.yml @@ -0,0 +1,2 @@ +feat: +- [Workspace]Refactor use case selector in workspace creation page ([#8413](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8413)) \ No newline at end of file diff --git a/src/core/utils/default_nav_groups.ts b/src/core/utils/default_nav_groups.ts index 76fedcf803ea..b58bd43e5b08 100644 --- a/src/core/utils/default_nav_groups.ts +++ b/src/core/utils/default_nav_groups.ts @@ -38,10 +38,11 @@ const defaultNavGroups = { all: { id: ALL_USE_CASE_ID, title: i18n.translate('core.ui.group.all.title', { - defaultMessage: 'Analytics (All)', + defaultMessage: 'Analytics', }), description: i18n.translate('core.ui.group.all.description', { - defaultMessage: 'This is a use case contains all the features.', + defaultMessage: + 'If you aren’t sure where to start with OpenSearch, or if you have needs that cut across multiple use cases.', }), order: 3000, icon: 'wsAnalytics', @@ -52,7 +53,8 @@ const defaultNavGroups = { defaultMessage: 'Observability', }), description: i18n.translate('core.ui.group.observability.description', { - defaultMessage: 'Gain visibility into your applications and infrastructure.', + defaultMessage: + 'Gain visibility into system health, performance, and reliability through monitoring of logs, metrics and traces.', }), order: 4000, icon: 'wsObservability', @@ -63,7 +65,8 @@ const defaultNavGroups = { defaultMessage: 'Security Analytics', }), description: i18n.translate('core.ui.group.security.analytics.description', { - defaultMessage: 'Enhance your security posture with advanced analytics.', + defaultMessage: + 'Detect and investigate potential security threats and vulnerabilities across your systems and data.', }), order: 5000, icon: 'wsSecurityAnalytics', @@ -86,7 +89,8 @@ const defaultNavGroups = { defaultMessage: 'Search', }), description: i18n.translate('core.ui.group.search.description', { - defaultMessage: 'Discover and query your data with ease.', + defaultMessage: + "Quickly find and explore relevant information across your organization's data sources.", }), order: 6000, icon: 'wsSearch', diff --git a/src/plugins/workspace/public/components/use_case_overview/setup_overview.test.tsx b/src/plugins/workspace/public/components/use_case_overview/setup_overview.test.tsx index a895534e47a9..ea0a12b930cf 100644 --- a/src/plugins/workspace/public/components/use_case_overview/setup_overview.test.tsx +++ b/src/plugins/workspace/public/components/use_case_overview/setup_overview.test.tsx @@ -141,7 +141,7 @@ describe('Setup use case overview', () => { "titleElement": "h4", "titleSize": "s", }, - "description": "Gain visibility into your applications and infrastructure.", + "description": "Gain visibility into system health, performance, and reliability through monitoring of logs, metrics and traces.", "getIcon": [Function], "id": "observability", "kind": "card", diff --git a/src/plugins/workspace/public/components/workspace_creator/creator_details_panel.tsx b/src/plugins/workspace/public/components/workspace_creator/creator_details_panel.tsx index 64519d9a1bde..f654ea327fd4 100644 --- a/src/plugins/workspace/public/components/workspace_creator/creator_details_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/creator_details_panel.tsx @@ -13,6 +13,7 @@ import { EuiFormControlLayout, EuiFormRow, EuiPanel, + EuiSpacer, EuiText, } from '@elastic/eui'; import { EuiColorPickerOutput } from '@elastic/eui/src/components/color_picker/color_picker'; @@ -46,6 +47,7 @@ export const CreatorDetailsPanel = ({ })} + diff --git a/src/plugins/workspace/public/components/workspace_form/index.ts b/src/plugins/workspace/public/components/workspace_form/index.ts index a58b2918a1b3..fef18d7c8c5e 100644 --- a/src/plugins/workspace/public/components/workspace_form/index.ts +++ b/src/plugins/workspace/public/components/workspace_form/index.ts @@ -12,6 +12,7 @@ export { WorkspaceCancelModal } from './workspace_cancel_modal'; export { WorkspaceNameField, WorkspaceDescriptionField } from './fields'; export { ConnectionTypeIcon } from './connection_type_icon'; export { DataSourceConnectionTable } from './data_source_connection_table'; +export { WorkspaceUseCaseFlyout } from './workspace_use_case_flyout'; export { WorkspaceFormSubmitData, WorkspaceFormProps, WorkspaceFormDataState } from './types'; export { diff --git a/src/plugins/workspace/public/components/workspace_form/types.ts b/src/plugins/workspace/public/components/workspace_form/types.ts index f160f18b4357..9c8da46e5be9 100644 --- a/src/plugins/workspace/public/components/workspace_form/types.ts +++ b/src/plugins/workspace/public/components/workspace_form/types.ts @@ -88,7 +88,10 @@ export interface WorkspaceFormProps { } export interface AvailableUseCaseItem - extends Pick { + extends Pick< + WorkspaceUseCase, + 'id' | 'title' | 'features' | 'description' | 'systematic' | 'icon' + > { disabled?: boolean; } diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.test.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.test.tsx index 8a0b14782e91..85725c452051 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.test.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.test.tsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { fireEvent, render, waitFor, within } from '@testing-library/react'; import { DEFAULT_NAV_GROUPS } from '../../../../../core/public'; import { WorkspaceUseCase, WorkspaceUseCaseProps } from './workspace_use_case'; import { WorkspaceFormErrors } from './types'; @@ -76,13 +76,13 @@ describe('WorkspaceUseCase', () => { ], }); await waitFor(() => { - expect(renderResult.getByText('Essentials')).toHaveClass( + expect(renderResult.getByText('Essentials').closest('label')).toHaveClass( 'euiCheckableCard__label-isDisabled' ); }); }); - it('should be able to toggle use case features', async () => { + it('should open flyout and expanded selected use cases', async () => { const { renderResult } = setup({ availableUseCases: [ { @@ -93,58 +93,46 @@ describe('WorkspaceUseCase', () => { ], }, ], + value: DEFAULT_NAV_GROUPS.observability.id, }); + + fireEvent.click(renderResult.getByText('Learn more.')); + await waitFor(() => { - expect(renderResult.getByText('See more....')).toBeInTheDocument(); - expect(renderResult.queryByText('Feature 1')).toBe(null); - expect(renderResult.queryByText('Feature 2')).toBe(null); + expect(within(renderResult.getByRole('dialog')).getByText('Use cases')).toBeInTheDocument(); + expect( + within(renderResult.getByRole('dialog')).getByText('Observability') + ).toBeInTheDocument(); + expect(within(renderResult.getByRole('dialog')).getByText('Feature 1')).toBeInTheDocument(); + expect(within(renderResult.getByRole('dialog')).getByText('Feature 2')).toBeInTheDocument(); }); + }); - fireEvent.click(renderResult.getByText('See more....')); + it('should close flyout after close button clicked', async () => { + const { renderResult } = setup({}); + fireEvent.click(renderResult.getByText('Learn more.')); await waitFor(() => { - expect(renderResult.getByText('See less....')).toBeInTheDocument(); - expect(renderResult.getByText('Feature 1')).toBeInTheDocument(); - expect(renderResult.getByText('Feature 2')).toBeInTheDocument(); + expect(within(renderResult.getByRole('dialog')).getByText('Use cases')).toBeInTheDocument(); }); - fireEvent.click(renderResult.getByText('See less....')); + fireEvent.click(renderResult.getByTestId('euiFlyoutCloseButton')); await waitFor(() => { - expect(renderResult.getByText('See more....')).toBeInTheDocument(); - expect(renderResult.queryByText('Feature 1')).toBe(null); - expect(renderResult.queryByText('Feature 2')).toBe(null); + expect(renderResult.queryByText('dialog')).toBeNull(); }); }); - it('should show static all use case features', async () => { + it('should render "(all features)" suffix for "all use case"', () => { const { renderResult } = setup({ availableUseCases: [ { ...DEFAULT_NAV_GROUPS.all, - features: [ - { id: 'feature1', title: 'Feature 1' }, - { id: 'feature2', title: 'Feature 2' }, - ], + features: [], }, ], }); - fireEvent.click(renderResult.getByText('See more....')); - - await waitFor(() => { - expect(renderResult.getByText('Discover')).toBeInTheDocument(); - expect(renderResult.getByText('Dashboards')).toBeInTheDocument(); - expect(renderResult.getByText('Visualize')).toBeInTheDocument(); - expect( - renderResult.getByText('Observability services, metrics, traces, and more') - ).toBeInTheDocument(); - expect( - renderResult.getByText('Security analytics threat alerts, findings, correlations, and more') - ).toBeInTheDocument(); - expect( - renderResult.getByText('Search studio, relevance tuning, vector search, and more') - ).toBeInTheDocument(); - }); + expect(renderResult.getByText('(all features)')).toBeInTheDocument(); }); }); diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx index 5eebdc8fa369..94defbdaefad 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useState, useMemo } from 'react'; +import React, { useCallback, useState } from 'react'; import { i18n } from '@osd/i18n'; import { EuiCheckableCard, @@ -12,14 +12,19 @@ import { EuiCompressedFormRow, EuiText, EuiLink, + EuiPanel, + EuiIcon, + EuiSpacer, } from '@elastic/eui'; +import { ALL_USE_CASE_ID } from '../../../../../core/public'; -import { ALL_USE_CASE_ID, DEFAULT_NAV_GROUPS } from '../../../../../core/public'; import { WorkspaceFormErrors, AvailableUseCaseItem } from './types'; +import { WorkspaceUseCaseFlyout } from './workspace_use_case_flyout'; import './workspace_use_case.scss'; interface WorkspaceUseCaseCardProps { id: string; + icon?: string; title: string; checked: boolean; disabled?: boolean; @@ -30,92 +35,57 @@ interface WorkspaceUseCaseCardProps { const WorkspaceUseCaseCard = ({ id, + icon, title, - features, description, checked, disabled, onChange, }: WorkspaceUseCaseCardProps) => { - const [isExpanded, setIsExpanded] = useState(false); - const featureItems = useMemo(() => { - if (id === DEFAULT_NAV_GROUPS.essentials.id) { - return []; - } - if (id === ALL_USE_CASE_ID) { - return [ - i18n.translate('workspace.form.useCase.feature.all.discover', { - defaultMessage: 'Discover', - }), - i18n.translate('workspace.form.useCase.feature.all.dashboards', { - defaultMessage: 'Dashboards', - }), - i18n.translate('workspace.form.useCase.feature.all.visualize', { - defaultMessage: 'Visualize', - }), - i18n.translate('workspace.form.useCase.feature.all.observability', { - defaultMessage: 'Observability services, metrics, traces, and more', - }), - i18n.translate('workspace.form.useCase.feature.all.securityAnalytics', { - defaultMessage: 'Security analytics threat alerts, findings, correlations, and more', - }), - i18n.translate('workspace.form.useCase.feature.all.search', { - defaultMessage: 'Search studio, relevance tuning, vector search, and more', - }), - ]; - } - - const featureTitles = features.flatMap((feature) => (feature.title ? [feature.title] : [])); - return featureTitles; - }, [features, id]); - const handleChange = useCallback(() => { onChange(id); }, [id, onChange]); - const toggleExpanded = useCallback(() => { - setIsExpanded((flag) => !flag); - }, []); return ( + {icon && ( + + + + )} + + +

+ {title} + {id === ALL_USE_CASE_ID && ( + <> +   + + + {i18n.translate('workspace.forms.useCaseCard.allUseCaseSuffix', { + defaultMessage: '(all features)', + })} + + + + )} +

+
+
+ + } checked={checked} className="workspace-use-case-item" onChange={handleChange} data-test-subj={`workspaceUseCase-${id}`} disabled={disabled} + style={{ width: '100%' }} > {description} - {featureItems.length > 0 && ( - - {isExpanded && ( - <> - {i18n.translate('workspace.form.useCase.featureExpandedTitle', { - defaultMessage: 'Feature includes:', - })} -
    - {featureItems.map((feature, index) => ( -
  • {feature}
  • - ))} -
- - )} - - - {isExpanded - ? i18n.translate('workspace.form.useCase.showLessButton', { - defaultMessage: 'See less....', - }) - : i18n.translate('workspace.form.useCase.showMoreButton', { - defaultMessage: 'See more....', - })} - - -
- )}
); }; @@ -133,29 +103,68 @@ export const WorkspaceUseCase = ({ formErrors, availableUseCases, }: WorkspaceUseCaseProps) => { + const [isUseCaseFlyoutVisible, setIsUseCaseFlyoutVisible] = useState(false); + const handleLearnMoreClick = useCallback(() => { + setIsUseCaseFlyoutVisible(true); + }, []); + const handleFlyoutClose = useCallback(() => { + setIsUseCaseFlyoutVisible(false); + }, []); + return ( - - - {availableUseCases.map(({ id, title, description, features, disabled }) => ( - - - - ))} - - + + +

+ {i18n.translate('workspace.form.panels.useCase.title', { + defaultMessage: 'Use case and features', + })} +

+
+ + {i18n.translate('workspace.form.panels.useCase.description', { + defaultMessage: + 'The use case defines the set of features that will be available in the workspace. You can change the use case later only to one with more features than the current use case.', + })} +   + + {i18n.translate('workspace.form.panels.useCase.learnMore', { + defaultMessage: 'Learn more.', + })} + + {isUseCaseFlyoutVisible && ( + + )} + + + + + {availableUseCases.map(({ id, icon, title, description, features, disabled }) => ( + + + + ))} + + +
); }; diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.test.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.test.tsx new file mode 100644 index 000000000000..2cc4efd8cf6d --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { DEFAULT_NAV_GROUPS } from '../../../../../core/public'; + +import { WorkspaceUseCaseFlyout } from './workspace_use_case_flyout'; + +const mockAvailableUseCases = [ + { + id: 'use-case-1', + icon: 'logoElasticsearch', + title: 'Use Case 1', + description: 'This is the description for Use Case 1', + features: [ + { + id: 'feature-1', + title: 'Feature 1', + details: ['Detail 1', 'Detail 2'], + }, + { + id: 'feature-2', + title: 'Feature 2', + details: [], + }, + ], + }, + { + id: 'use-case-2', + icon: 'logoKibana', + title: 'Use Case 2', + description: 'This is the description for Use Case 2', + features: [], + }, +]; + +describe('WorkspaceUseCaseFlyout', () => { + it('should render the flyout with the correct title and available use cases', () => { + render( + + ); + const title = screen.getByText('Use cases'); + expect(title).toBeInTheDocument(); + expect(screen.getByText(mockAvailableUseCases[0].title)).toBeInTheDocument(); + expect(screen.getByText(mockAvailableUseCases[0].title)).toBeInTheDocument(); + }); + + it('should call the onClose callback when the close button is clicked', () => { + const onCloseMock = jest.fn(); + render( + + ); + const closeButton = screen.getByTestId('euiFlyoutCloseButton'); + fireEvent.click(closeButton); + expect(onCloseMock).toHaveBeenCalled(); + }); + + it('should expand the default use case if provided', () => { + render( + + ); + const useCaseDescription = screen.getByText(/This is the description for Use Case 1/); + expect(useCaseDescription).toBeInTheDocument(); + }); + + it('should render "(all features)" suffix for "all use case"', () => { + render( + + ); + expect(screen.getByText('(all features)')).toBeInTheDocument(); + }); +}); diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.tsx new file mode 100644 index 000000000000..7617f3ec14ea --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_use_case_flyout.tsx @@ -0,0 +1,128 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiAccordion, + EuiSpacer, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, +} from '@elastic/eui'; +import { ALL_USE_CASE_ID } from '../../../../../core/public'; + +import { AvailableUseCaseItem } from './types'; + +const WORKSPACE_USE_CASE_FLYOUT_TITLE_ID = 'workspaceUseCaseFlyoutTitle'; + +export interface WorkspaceUseCaseFlyoutProps { + onClose: () => void; + availableUseCases: AvailableUseCaseItem[]; + defaultExpandUseCase?: string; +} + +export const WorkspaceUseCaseFlyout = ({ + onClose, + availableUseCases, + defaultExpandUseCase, +}: WorkspaceUseCaseFlyoutProps) => { + return ( + + + +

+ {i18n.translate('workspace.forms.useCaseFlyout.title', { defaultMessage: 'Use cases' })} +

+
+
+ + {availableUseCases.map(({ id, icon, title, description, features }, index) => ( + + + {icon && ( + + + + )} + + +

+ {title} + {id === ALL_USE_CASE_ID && ( + <> +   + + + {i18n.translate('workspace.forms.useCaseFlyout.allUseCaseSuffix', { + defaultMessage: '(all features)', + })} + + + + )} +

+
+
+ + } + paddingSize="l" + initialIsOpen={id === defaultExpandUseCase} + > + + {description} +
+ {features && features.length > 0 && ( + <> + {i18n.translate('workspace.forms.useCaseFlyout.featuresIncluded', { + defaultMessage: 'Features included:', + })} +
+
    + {features.map(({ id: featureId, title: featureTitle, details }) => ( +
  • + + {i18n.translate('workspace.forms.useCaseFlyout.featureTitle', { + defaultMessage: + '{featureTitle}{detailsCount, plural, =0 {} other {: }}', + values: { featureTitle, detailsCount: details?.length ?? 0 }, + })} + + {details?.join( + i18n.translate( + 'workspace.forms.useCaseFlyout.featuresDetails.delimiter', + { + defaultMessage: ', ', + } + ) + )} +
  • + ))} +
+ + )} +
+
+ {index < availableUseCases.length - 1 && } +
+ ))} +
+
+ ); +}; diff --git a/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap b/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap index 0bb3f51ead3d..57c41452231e 100644 --- a/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap +++ b/src/plugins/workspace/public/components/workspace_list/__snapshots__/default_workspace.test.tsx.snap @@ -248,7 +248,7 @@ exports[`UserDefaultWorkspace should render title and table normally 1`] = ` - Analytics (All) + Analytics diff --git a/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap b/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap index b32655bafd98..b30c67b37643 100644 --- a/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap +++ b/src/plugins/workspace/public/components/workspace_list/__snapshots__/index.test.tsx.snap @@ -411,7 +411,7 @@ exports[`WorkspaceList should render title and table normally 1`] = ` - Analytics (All) + Analytics diff --git a/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx b/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx index f6a50b1c1350..eeeec9e06fb0 100644 --- a/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx +++ b/src/plugins/workspace/public/components/workspace_list/default_workspace.test.tsx @@ -137,7 +137,7 @@ describe('UserDefaultWorkspace', () => { expect(getByText('name2')).toBeInTheDocument(); // should display use case - expect(getByText('Analytics (All)')).toBeInTheDocument(); + expect(getByText('Analytics')).toBeInTheDocument(); expect(getByText('Observability')).toBeInTheDocument(); // owner column not display diff --git a/src/plugins/workspace/public/components/workspace_list/index.test.tsx b/src/plugins/workspace/public/components/workspace_list/index.test.tsx index a945866fd7bf..62709e4fe053 100644 --- a/src/plugins/workspace/public/components/workspace_list/index.test.tsx +++ b/src/plugins/workspace/public/components/workspace_list/index.test.tsx @@ -153,7 +153,7 @@ describe('WorkspaceList', () => { expect(getByText('name2')).toBeInTheDocument(); // should display use case - expect(getByText('Analytics (All)')).toBeInTheDocument(); + expect(getByText('Analytics')).toBeInTheDocument(); expect(getByText('Observability')).toBeInTheDocument(); }); diff --git a/src/plugins/workspace/public/services/use_case_service.test.ts b/src/plugins/workspace/public/services/use_case_service.test.ts index bdd819145f82..d810dc570bb3 100644 --- a/src/plugins/workspace/public/services/use_case_service.test.ts +++ b/src/plugins/workspace/public/services/use_case_service.test.ts @@ -47,12 +47,18 @@ const setupUseCaseStart = (options?: { navGroupEnabled?: boolean }) => { ]); const navGroupsMap$ = new BehaviorSubject>(mockNavGroupsMap); const useCase = new UseCaseService(); + const navLinks$ = new BehaviorSubject([ + { id: 'dashboards', title: 'Dashboards', baseUrl: '', href: '' }, + { id: 'searchRelevance', title: 'Search Relevance', baseUrl: '', href: '' }, + ]); chrome.navGroup.getNavGroupEnabled.mockImplementation(() => options?.navGroupEnabled ?? true); chrome.navGroup.getNavGroupsMap$.mockImplementation(() => navGroupsMap$); + chrome.navLinks.getNavLinks$.mockImplementation(() => navLinks$); return { chrome, + navLinks$, navGroupsMap$, workspaceConfigurableApps$, useCaseStart: useCase.start({ @@ -187,6 +193,23 @@ describe('UseCaseService', () => { }); expect(fn).toHaveBeenCalledTimes(2); }); + + it('should not emit after navLinks$ emit same value', async () => { + const { useCaseStart, navLinks$ } = setupUseCaseStart(); + const registeredUseCases$ = useCaseStart.getRegisteredUseCases$(); + const fn = jest.fn(); + + registeredUseCases$.subscribe(fn); + + expect(fn).toHaveBeenCalledTimes(1); + + navLinks$.next([...navLinks$.getValue()]); + expect(fn).toHaveBeenCalledTimes(1); + + navLinks$.next([...navLinks$.getValue()].slice(1)); + expect(fn).toHaveBeenCalledTimes(2); + }); + it('should move all use case to the last one', async () => { const { useCaseStart, navGroupsMap$ } = setupUseCaseStart(); diff --git a/src/plugins/workspace/public/services/use_case_service.ts b/src/plugins/workspace/public/services/use_case_service.ts index 31e15977d9a5..ca4f2b55c223 100644 --- a/src/plugins/workspace/public/services/use_case_service.ts +++ b/src/plugins/workspace/public/services/use_case_service.ts @@ -118,12 +118,13 @@ export class UseCaseService { return { getRegisteredUseCases$: () => { if (chrome.navGroup.getNavGroupEnabled()) { - return chrome.navGroup - .getNavGroupsMap$() + return combineLatest([chrome.navGroup.getNavGroupsMap$(), chrome.navLinks.getNavLinks$()]) .pipe( - map((navGroupsMap) => - Object.values(navGroupsMap).map(convertNavGroupToWorkspaceUseCase) - ) + map(([navGroupsMap, allNavLinks]) => { + return Object.values(navGroupsMap).map((navGroup) => + convertNavGroupToWorkspaceUseCase(navGroup, allNavLinks) + ); + }) ) .pipe( distinctUntilChanged((useCases, anotherUseCases) => { @@ -169,17 +170,23 @@ export class UseCaseService { .filter((useCase) => { return useCase.features.some((featureId) => configurableAppsId.includes(featureId)); }) - .map((item) => ({ - ...item, - features: item.features.map((featureId) => ({ - title: configurableApps.find((app) => app.id === featureId)?.title, - id: featureId, - })), - })) + .map( + (item) => + ({ + ...item, + features: item.features.map((featureId) => ({ + title: configurableApps.find((app) => app.id === featureId)?.title, + id: featureId, + })), + } as WorkspaceUseCase) + ) .concat({ ...DEFAULT_NAV_GROUPS.all, - features: configurableApps.map((app) => ({ id: app.id, title: app.title })), - }) as WorkspaceUseCase[]; + features: configurableApps.map((app) => ({ + id: app.id, + title: app.title, + })), + } as WorkspaceUseCase); }) ); }, diff --git a/src/plugins/workspace/public/types.ts b/src/plugins/workspace/public/types.ts index 53f2be4dfb36..997e8c67fd05 100644 --- a/src/plugins/workspace/public/types.ts +++ b/src/plugins/workspace/public/types.ts @@ -20,6 +20,7 @@ export type Services = CoreStart & { export interface WorkspaceUseCaseFeature { id: string; title?: string; + details?: string[]; } export interface WorkspaceUseCase { diff --git a/src/plugins/workspace/public/utils.test.ts b/src/plugins/workspace/public/utils.test.ts index 9f79beafea12..a5f2a9c435e2 100644 --- a/src/plugins/workspace/public/utils.test.ts +++ b/src/plugins/workspace/public/utils.test.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { AppNavLinkStatus, NavGroupType, PublicAppInfo } from '../../../core/public'; +import { AppNavLinkStatus, ChromeNavLink, NavGroupType, PublicAppInfo } from '../../../core/public'; import { featureMatchesConfig, filterWorkspaceConfigurableApps, @@ -19,7 +19,7 @@ import { } from './utils'; import { WorkspaceAvailability } from '../../../core/public'; import { coreMock } from '../../../core/public/mocks'; -import { WORKSPACE_DETAIL_APP_ID, USE_CASE_PREFIX } from '../common/constants'; +import { USE_CASE_PREFIX } from '../common/constants'; import { SigV4ServiceName, DataSourceEngineType, @@ -37,10 +37,23 @@ const useCaseMock = { id: 'foo', title: 'Foo', description: 'Foo description', - features: [{ id: 'bar' }], + features: [{ id: 'bar' }, { id: 'baz', title: 'Baz', details: ['Qux'] }], systematic: false, order: 1, }; +const allNavLinksMock: ChromeNavLink[] = [ + { id: 'foo', title: 'Foo', baseUrl: '', href: '' }, + { id: 'bar', title: 'Bar', baseUrl: '', href: '' }, + { id: 'baz', title: 'Baz', baseUrl: '', href: '' }, + { id: 'qux', title: 'Qux', baseUrl: '', href: '' }, + { id: 'observability_overview', title: 'Observability Overview', baseUrl: '', href: '' }, + { + id: 'observability-gettingStarted', + title: 'Observability Getting Started', + baseUrl: '', + href: '', + }, +]; describe('workspace utils: featureMatchesConfig', () => { it('feature configured with `*` should match any features', () => { @@ -530,13 +543,16 @@ describe('workspace utils: getIsOnlyAllowEssentialUseCase', () => { describe('workspace utils: convertNavGroupToWorkspaceUseCase', () => { it('should convert nav group to consistent workspace use case', () => { expect( - convertNavGroupToWorkspaceUseCase({ - id: 'foo', - title: 'Foo', - description: 'Foo description', - navLinks: [{ id: 'bar', title: 'Bar' }], - icon: 'wsAnalytics', - }) + convertNavGroupToWorkspaceUseCase( + { + id: 'foo', + title: 'Foo', + description: 'Foo description', + navLinks: [{ id: 'bar', title: 'Bar' }], + icon: 'wsAnalytics', + }, + allNavLinksMock + ) ).toEqual({ id: 'foo', title: 'Foo', @@ -547,13 +563,16 @@ describe('workspace utils: convertNavGroupToWorkspaceUseCase', () => { }); expect( - convertNavGroupToWorkspaceUseCase({ - id: 'foo', - title: 'Foo', - description: 'Foo description', - navLinks: [{ id: 'bar', title: 'Bar' }], - type: NavGroupType.SYSTEM, - }) + convertNavGroupToWorkspaceUseCase( + { + id: 'foo', + title: 'Foo', + description: 'Foo description', + navLinks: [{ id: 'bar', title: 'Bar' }], + type: NavGroupType.SYSTEM, + }, + allNavLinksMock + ) ).toEqual({ id: 'foo', title: 'Foo', @@ -562,6 +581,111 @@ describe('workspace utils: convertNavGroupToWorkspaceUseCase', () => { systematic: true, }); }); + + it('should filter out overview features', () => { + expect( + convertNavGroupToWorkspaceUseCase( + { + id: 'foo', + title: 'Foo', + description: 'Foo description', + navLinks: [{ id: 'observability_overview', title: 'Observability Overview' }], + icon: 'wsAnalytics', + }, + allNavLinksMock + ) + ).toEqual( + expect.objectContaining({ + id: 'foo', + title: 'Foo', + description: 'Foo description', + features: [], + systematic: false, + icon: 'wsAnalytics', + }) + ); + }); + + it('should filter out getting started features', () => { + expect( + convertNavGroupToWorkspaceUseCase( + { + id: 'foo', + title: 'Foo', + description: 'Foo description', + navLinks: [ + { id: 'observability-gettingStarted', title: 'Observability Getting Started' }, + ], + icon: 'wsAnalytics', + }, + allNavLinksMock + ) + ).toEqual( + expect.objectContaining({ + id: 'foo', + title: 'Foo', + description: 'Foo description', + features: [], + systematic: false, + icon: 'wsAnalytics', + }) + ); + }); + + it('should grouped nav links by category', () => { + expect( + convertNavGroupToWorkspaceUseCase( + { + id: 'foo', + title: 'Foo', + description: 'Foo description', + navLinks: [ + { id: 'bar', title: 'Bar', category: { id: 'category-1', label: 'Category 1' } }, + { id: 'baz', title: 'Baz' }, + { id: 'qux', title: 'Qux', category: { id: 'category-1', label: 'Category 1' } }, + ], + icon: 'wsAnalytics', + }, + allNavLinksMock + ) + ).toEqual( + expect.objectContaining({ + id: 'foo', + title: 'Foo', + description: 'Foo description', + features: [ + { id: 'baz', title: 'Baz' }, + { id: 'category-1', title: 'Category 1', details: ['Bar', 'Qux'] }, + ], + systematic: false, + icon: 'wsAnalytics', + }) + ); + }); + + it('should filter out custom features', () => { + expect( + convertNavGroupToWorkspaceUseCase( + { + id: 'foo', + title: 'Foo', + description: 'Foo description', + navLinks: [{ id: 'bar', title: 'Bar', category: { id: 'custom', label: 'Custom' } }], + icon: 'wsAnalytics', + }, + allNavLinksMock + ) + ).toEqual( + expect.objectContaining({ + id: 'foo', + title: 'Foo', + description: 'Foo description', + features: [], + systematic: false, + icon: 'wsAnalytics', + }) + ); + }); }); describe('workspace utils: isEqualWorkspaceUseCase', () => { @@ -661,6 +785,28 @@ describe('workspace utils: isEqualWorkspaceUseCase', () => { ) ).toEqual(true); }); + + it('should return false for different feature details', () => { + const featureWithDetails = { + id: 'foo', + title: 'Foo', + details: ['Bar'], + }; + const featureWithOtherDetails = { + id: 'foo', + title: 'Foo', + details: ['Baz'], + }; + expect( + isEqualWorkspaceUseCase( + { ...useCaseMock, features: [featureWithDetails] }, + { + ...useCaseMock, + features: [featureWithOtherDetails], + } + ) + ).toEqual(false); + }); it('should return true when all properties equal', () => { expect( isEqualWorkspaceUseCase(useCaseMock, { diff --git a/src/plugins/workspace/public/utils.ts b/src/plugins/workspace/public/utils.ts index 9cadc2f54054..77c005cd7eea 100644 --- a/src/plugins/workspace/public/utils.ts +++ b/src/plugins/workspace/public/utils.ts @@ -15,8 +15,8 @@ import { ApplicationStart, HttpSetup, NotificationsStart, -} from '../../../core/public'; -import { + fulfillRegistrationLinksToChromeNavLinks, + ChromeNavLink, App, AppCategory, AppNavLinkStatus, @@ -360,23 +360,53 @@ export const getIsOnlyAllowEssentialUseCase = async (client: SavedObjectsStart[' return false; }; -export const convertNavGroupToWorkspaceUseCase = ({ - id, - title, - description, - navLinks, - type, - order, - icon, -}: NavGroupItemInMap): WorkspaceUseCase => ({ - id, - title, - description, - features: navLinks.map((item) => ({ id: item.id, title: item.title })), - systematic: type === NavGroupType.SYSTEM || id === ALL_USE_CASE_ID, - order, - icon, -}); +export const convertNavGroupToWorkspaceUseCase = ( + { id, title, description, navLinks, type, order, icon }: NavGroupItemInMap, + allNavLinks: ChromeNavLink[] +): WorkspaceUseCase => { + const visibleNavLinks = allNavLinks.filter((link) => !link.hidden); + const visibleNavLinksWithinNavGroup = fulfillRegistrationLinksToChromeNavLinks( + navLinks, + visibleNavLinks + ); + const features: WorkspaceUseCaseFeature[] = []; + const category2NavLinks: { [key: string]: WorkspaceUseCaseFeature & { details: string[] } } = {}; + for (const { id: featureId, title: featureTitle, category } of visibleNavLinksWithinNavGroup) { + const lowerFeatureId = featureId.toLowerCase(); + // Filter out overview and getting started links + if (lowerFeatureId.endsWith('overview') || lowerFeatureId.endsWith('started')) { + continue; + } + if (!category) { + features.push({ id: featureId, title: featureTitle }); + continue; + } + // Filter out custom features + if (category.id === 'custom') { + continue; + } + if (!category2NavLinks[category.id]) { + category2NavLinks[category.id] = { + id: category.id, + title: category.label, + details: [], + }; + } + if (featureTitle) { + category2NavLinks[category.id].details.push(featureTitle); + } + } + features.push(...Object.values(category2NavLinks)); + return { + id, + title, + description, + features, + systematic: type === NavGroupType.SYSTEM || id === ALL_USE_CASE_ID, + order, + icon, + }; +}; const compareFeatures = ( features1: WorkspaceUseCaseFeature[], @@ -384,7 +414,7 @@ const compareFeatures = ( ) => { const featuresSerializer = (features: WorkspaceUseCaseFeature[]) => features - .map(({ id, title }) => `${id}-${title}`) + .map(({ id, title, details }) => `${id}-${title}-${details?.join('')}`) .sort() .join(); return featuresSerializer(features1) === featuresSerializer(features2);