diff --git a/changelogs/fragments/7401.yml b/changelogs/fragments/7401.yml new file mode 100644 index 000000000000..5aebdb156692 --- /dev/null +++ b/changelogs/fragments/7401.yml @@ -0,0 +1,2 @@ +fix: +- Make breadcrumb of 4 new added applications comply with BrowserRouter. ([#7401](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7401)) \ No newline at end of file diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_group_enabled.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_group_enabled.test.tsx.snap index 1bd0458ef1b5..4201e5146669 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_group_enabled.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav_group_enabled.test.tsx.snap @@ -1,34 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` should hide left navigation when in home page when workspace is enabled 1`] = ` -
-
-
-
-
-
-
-
-
-
-
-`; - exports[` should render correctly 1`] = `
should show all use case by default and
`; +exports[` should show all use case when current nav group is \`all\` 1`] = ` +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+`; + exports[` should render correctly 1`] = `
', () => { function mockProps( props?: Partial & { navGroupsMap?: Record; + currentNavGroupId?: string; + navLinks?: ChromeNavLink[]; } ): CollapsibleNavGroupEnabledProps { - const currentNavGroup$ = new BehaviorSubject(undefined); const navGroupsMap$ = new BehaviorSubject>({ [ALL_USE_CASE_ID]: { ...DEFAULT_NAV_GROUPS[ALL_USE_CASE_ID], @@ -121,6 +122,9 @@ describe('', () => { }, ...props?.navGroupsMap, }); + const currentNavGroup$ = new BehaviorSubject( + props?.currentNavGroupId ? navGroupsMap$.getValue()[props.currentNavGroupId] : undefined + ); return { appId$: new BehaviorSubject('test'), basePath: mockBasePath, @@ -146,6 +150,7 @@ describe('', () => { baseUrl: '', href: '', }, + ...(props?.navLinks || []), ]), storage: new StubBrowserStorage(), onIsLockedUpdate: () => {}, @@ -226,8 +231,9 @@ describe('', () => { expect(getAllByTestId('collapsibleNavAppLink-link-in-analytics').length).toEqual(2); }); - it('should hide left navigation when in home page when workspace is enabled', async () => { + it('should show all use case when current nav group is `all`', async () => { const props = mockProps({ + currentNavGroupId: ALL_USE_CASE_ID, navGroupsMap: { [DEFAULT_NAV_GROUPS.analytics.id]: { ...DEFAULT_NAV_GROUPS.analytics, @@ -241,12 +247,45 @@ describe('', () => { }, }, }); - props.appId$ = new BehaviorSubject('home'); - if (props.capabilities.workspaces) { - (props.capabilities.workspaces as Record) = {}; - (props.capabilities.workspaces as Record).enabled = true; - } - const { container } = render(); + const { container, getAllByTestId, getByTestId } = render( + + ); + fireEvent.click(getAllByTestId('collapsibleNavAppLink-link-in-analytics')[1]); + expect(getAllByTestId('collapsibleNavAppLink-link-in-analytics').length).toEqual(1); expect(container).toMatchSnapshot(); + fireEvent.click(getByTestId('back')); + expect(getAllByTestId('collapsibleNavAppLink-link-in-analytics').length).toEqual(2); + }); + + it('should not show group if the nav link is hidden', async () => { + const props = mockProps({ + currentNavGroupId: ALL_USE_CASE_ID, + navGroupsMap: { + [DEFAULT_NAV_GROUPS.analytics.id]: { + ...DEFAULT_NAV_GROUPS.analytics, + navLinks: [ + { + id: 'link-in-analytics-but-hidden', + title: 'link-in-analytics-but-hidden', + showInAllNavGroup: true, + }, + ], + }, + }, + navLinks: [ + { + id: 'link-in-analytics-but-hidden', + hidden: true, + title: 'link-in-analytics-but-hidden', + baseUrl: '', + href: '', + }, + ], + }); + const { queryAllByTestId } = render(); + expect(queryAllByTestId('collapsibleNavAppLink-link-in-analytics-but-hidden').length).toEqual( + 0 + ); + expect(queryAllByTestId('collapsibleNavAppLink-link-in-all').length).toEqual(1); }); }); diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx index c6669a6b99f8..ea5510ad5994 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled.tsx @@ -177,7 +177,7 @@ export function CollapsibleNavGroupEnabled({ basePath, id, isLocked, - isNavOpen: isNavOpenProps, + isNavOpen, storage = window.localStorage, onIsLockedUpdate, closeNav, @@ -194,7 +194,7 @@ export function CollapsibleNavGroupEnabled({ const currentNavGroup = useObservable(observables.currentNavGroup$, undefined); const navLinksForRender: ChromeNavLink[] = useMemo(() => { - if (currentNavGroup) { + if (currentNavGroup && currentNavGroup.id !== ALL_USE_CASE_ID) { return fulfillRegistrationLinksToChromeNavLinks( navGroupsMap[currentNavGroup.id].navLinks || [], navLinks @@ -241,7 +241,10 @@ export function CollapsibleNavGroupEnabled({ label: group.title, order: group.order, }; - const linksForAllUseCaseWithinNavGroup = group.navLinks + const linksForAllUseCaseWithinNavGroup = fulfillRegistrationLinksToChromeNavLinks( + group.navLinks, + navLinks + ) .filter((navLink) => navLink.showInAllNavGroup) .map((navLink) => ({ ...navLink, @@ -263,18 +266,6 @@ export function CollapsibleNavGroupEnabled({ return fulfillRegistrationLinksToChromeNavLinks(navLinksForAll, navLinks); }, [navLinks, navGroupsMap, currentNavGroup]); - const isNavOpen = useMemo(() => { - // For now, only home page need to always collapse left navigation - // when workspace is enabled. - // If there are more pages need to collapse left navigation in the future - // need to come up with a mechanism to register. - if (capabilities.workspaces.enabled && appId === 'home') { - return false; - } - - return isNavOpenProps; - }, [isNavOpenProps, capabilities.workspaces.enabled, appId]); - const width = useMemo(() => { if (!isNavOpen) { return 50; diff --git a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.tsx b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.tsx index 9e89155a8e4e..568290da727b 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav_group_enabled_top.tsx @@ -18,6 +18,7 @@ import { i18n } from '@osd/i18n'; import { createEuiListItem } from './nav_link'; import { NavGroupItemInMap } from '../../nav_group'; import { ChromeNavLink } from '../../nav_links'; +import { ALL_USE_CASE_ID } from '../../../../../core/utils'; export interface CollapsibleNavTopProps { navLinks: ChromeNavLink[]; @@ -44,6 +45,7 @@ export const CollapsibleNavTop = ({ const shouldShowBackButton = useMemo( () => + currentNavGroup?.id !== ALL_USE_CASE_ID && !shouldShrinkNavigation && Object.values(navGroupsMap).filter((item) => !item.type).length > 1 && currentNavGroup, diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index 4e3539f2e53b..bef0f152c6a4 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -187,4 +187,22 @@ describe('Header', () => { expect(component.find('CollapsibleNavGroupEnabled').exists()).toBeTruthy(); }); + + it('show hide expand icon in top left navigation when workspace enabled + homepage + new navigation enabled', () => { + const branding = { + useExpandedHeader: false, + }; + const props = { + ...mockProps(), + branding, + }; + props.application.currentAppId$ = new BehaviorSubject('home'); + props.application.capabilities = { ...props.application.capabilities }; + (props.application.capabilities.workspaces as Record) = {}; + (props.application.capabilities.workspaces as Record).enabled = true; + + const component = mountWithIntl(
); + + expect(component.find('.header__toggleNavButtonSection').exists()).toBeFalsy(); + }); }); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index dc161456ba84..adcbba00fe8c 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -131,9 +131,17 @@ export function Header({ }: HeaderProps) { const isVisible = useObservable(observables.isVisible$, false); const isLocked = useObservable(observables.isLocked$, false); + const appId = useObservable(application.currentAppId$, ''); const [isNavOpen, setIsNavOpen] = useState(false); const sidecarConfig = useObservable(observables.sidecarConfig$, undefined); + /** + * This is a workaround on 2.16 to hide the navigation items within left navigation + * when user is in homepage with workspace enabled + new navigation enabled + */ + const shouldHideExpandIcon = + navGroupEnabled && appId === 'home' && application.capabilities.workspaces.enabled; + const sidecarPaddingStyle = useMemo(() => { return getOsdSidecarPaddingStyle(sidecarConfig); }, [sidecarConfig]); @@ -198,29 +206,31 @@ export function Header({ - - - + setIsNavOpen(!isNavOpen)} - aria-expanded={isNavOpen} - aria-pressed={isNavOpen} - aria-controls={navId} - ref={toggleCollapsibleNavRef} + delay="long" + position="bottom" > - - - - + setIsNavOpen(!isNavOpen)} + aria-expanded={isNavOpen} + aria-pressed={isNavOpen} + aria-controls={navId} + ref={toggleCollapsibleNavRef} + > + + + + + )} @@ -293,7 +303,7 @@ export function Header({ id={navId} isLocked={isLocked} navLinks$={observables.navLinks$} - isNavOpen={isNavOpen} + isNavOpen={shouldHideExpandIcon ? false : isNavOpen} basePath={basePath} navigateToApp={application.navigateToApp} navigateToUrl={application.navigateToUrl} diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index f2bf05da4114..17cb7508c440 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -35,6 +35,7 @@ import { ComponentRegistry } from './component_registry'; import { AdvancedSettingsSetup, AdvancedSettingsStart, AdvancedSettingsPluginSetup } from './types'; import { setupTopNavThemeButton } from './register_nav_control'; import { DEFAULT_NAV_GROUPS, AppNavLinkStatus, WorkspaceAvailability } from '../../../core/public'; +import { getScopedBreadcrumbs } from '../../opensearch_dashboards_react/public'; const component = new ComponentRegistry(); @@ -81,7 +82,8 @@ export class AdvancedSettingsPlugin { ...params, basePath: core.http.basePath.get(), - setBreadcrumbs: coreStart.chrome.setBreadcrumbs, + setBreadcrumbs: (breadCrumbs) => + coreStart.chrome.setBreadcrumbs(getScopedBreadcrumbs(breadCrumbs, params.history)), wrapInPage: true, }, component.start diff --git a/src/plugins/data_source_management/public/plugin.ts b/src/plugins/data_source_management/public/plugin.ts index c51be57e4bb2..c384e30089f3 100644 --- a/src/plugins/data_source_management/public/plugin.ts +++ b/src/plugins/data_source_management/public/plugin.ts @@ -50,6 +50,7 @@ import { import { AccelerationDetailsFlyout } from './components/direct_query_data_sources_components/acceleration_management/acceleration_details_flyout'; import { CreateAcceleration } from './components/direct_query_data_sources_components/acceleration_creation/create/create_acceleration'; import { AssociatedObjectsDetailsFlyout } from './components/direct_query_data_sources_components/associated_object_management/associated_objects_details_flyout'; +import { getScopedBreadcrumbs } from '../../opensearch_dashboards_react/public'; export const [ getRenderAccelerationDetailsFlyout, @@ -141,11 +142,6 @@ export class DataSourceManagementPlugin }, }); - // when the feature flag is disabled, we don't need to register any of the mds components - if (!this.featureFlagStatus) { - return undefined; - } - /** * The data sources features in observability has the same name as `DSM_APP_ID` * Add a suffix to avoid duplication @@ -166,7 +162,8 @@ export class DataSourceManagementPlugin { ...params, basePath: core.http.basePath.get(), - setBreadcrumbs: coreStart.chrome.setBreadcrumbs, + setBreadcrumbs: (breadCrumbs) => + coreStart.chrome.setBreadcrumbs(getScopedBreadcrumbs(breadCrumbs, params.history)), wrapInPage: true, }, this.authMethodsRegistry, @@ -224,6 +221,11 @@ export class DataSourceManagementPlugin }, ]); + // when the feature flag is disabled, we don't need to register any of the mds components + if (!this.featureFlagStatus) { + return undefined; + } + const registerAuthenticationMethod = (authMethod: AuthenticationMethod) => { if (this.started) { throw new Error( diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index ef462374129e..36bc22c5d659 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -47,6 +47,7 @@ import { import { ManagementSetup } from '../../management/public'; import { DEFAULT_NAV_GROUPS, AppStatus, DEFAULT_APP_CATEGORIES } from '../../../core/public'; +import { getScopedBreadcrumbs } from '../../opensearch_dashboards_react/public'; export interface IndexPatternManagementSetupDependencies { management: ManagementSetup; @@ -148,7 +149,8 @@ export class IndexPatternManagementPlugin { ...params, basePath: core.http.basePath.get(), - setBreadcrumbs: coreStart.chrome.setBreadcrumbs, + setBreadcrumbs: (breadCrumbs) => + coreStart.chrome.setBreadcrumbs(getScopedBreadcrumbs(breadCrumbs, params.history)), wrapInPage: true, }, () => this.indexPatternManagementService.environmentService.getEnvironment().ml(), diff --git a/src/plugins/opensearch_dashboards_react/public/react_router_navigate/index.ts b/src/plugins/opensearch_dashboards_react/public/react_router_navigate/index.ts index 8ba6b1e2fe90..f5a3ecceac35 100644 --- a/src/plugins/opensearch_dashboards_react/public/react_router_navigate/index.ts +++ b/src/plugins/opensearch_dashboards_react/public/react_router_navigate/index.ts @@ -28,4 +28,8 @@ * under the License. */ -export { reactRouterNavigate, reactRouterOnClickHandler } from './react_router_navigate'; +export { + reactRouterNavigate, + reactRouterOnClickHandler, + getScopedBreadcrumbs, +} from './react_router_navigate'; diff --git a/src/plugins/opensearch_dashboards_react/public/react_router_navigate/react_router_navigate.test.tsx b/src/plugins/opensearch_dashboards_react/public/react_router_navigate/react_router_navigate.test.tsx new file mode 100644 index 000000000000..138e33906ac7 --- /dev/null +++ b/src/plugins/opensearch_dashboards_react/public/react_router_navigate/react_router_navigate.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { scopedHistoryMock } from '../../../../core/public/mocks'; +import { getScopedBreadcrumbs } from './react_router_navigate'; + +describe('getScopedBreadcrumbs', () => { + it('should return scoped bread crumbs when given an array', () => { + const history = scopedHistoryMock.create({ + pathname: '/base', + }); + history.createHref.mockImplementation((location) => `/base${location.pathname}`); + const scopedBreadcrumbs = getScopedBreadcrumbs( + [ + { + text: 'Home', + href: '/', + }, + { + text: 'Dashboard', + href: '/dashboard', + }, + ], + history + ); + expect(scopedBreadcrumbs[0]).toEqual( + expect.objectContaining({ + href: '/base/', + text: 'Home', + }) + ); + expect(scopedBreadcrumbs[1]).toEqual( + expect.objectContaining({ + href: '/base/dashboard', + text: 'Dashboard', + }) + ); + }); +}); diff --git a/src/plugins/opensearch_dashboards_react/public/react_router_navigate/react_router_navigate.tsx b/src/plugins/opensearch_dashboards_react/public/react_router_navigate/react_router_navigate.tsx index 5e367f8cb94b..ae249bdadea9 100644 --- a/src/plugins/opensearch_dashboards_react/public/react_router_navigate/react_router_navigate.tsx +++ b/src/plugins/opensearch_dashboards_react/public/react_router_navigate/react_router_navigate.tsx @@ -28,7 +28,7 @@ * under the License. */ -import { ScopedHistory } from 'opensearch-dashboards/public'; +import { ChromeBreadcrumb, ScopedHistory } from 'opensearch-dashboards/public'; import { History } from 'history'; interface LocationObject { @@ -79,3 +79,14 @@ export const reactRouterOnClickHandler = ( event.preventDefault(); history.push(toLocationObject(to)); }; + +export const getScopedBreadcrumbs = ( + crumbs: ChromeBreadcrumb[] = [], + appHistory: ScopedHistory +) => { + const wrapBreadcrumb = (item: ChromeBreadcrumb, scopedHistory: ScopedHistory) => ({ + ...item, + ...(item.href ? reactRouterNavigate(scopedHistory, item.href) : {}), + }); + return crumbs.map((item) => wrapBreadcrumb(item, appHistory)); +}; diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index 1247b56b5555..8529a9555681 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -66,6 +66,7 @@ import { bootstrap } from './ui_actions_bootstrap'; import { DEFAULT_NAV_GROUPS, DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { RecentWork } from './management_section/recent_work'; import { HOME_CONTENT_AREAS } from '../../../plugins/home/public'; +import { getScopedBreadcrumbs } from '../../opensearch_dashboards_react/public'; export interface SavedObjectsManagementPluginSetup { actions: SavedObjectsManagementActionServiceSetup; @@ -173,7 +174,8 @@ export class SavedObjectsManagementPlugin mountParams: { ...params, basePath: core.http.basePath.get(), - setBreadcrumbs: coreStart.chrome.setBreadcrumbs, + setBreadcrumbs: (breadCrumbs) => + coreStart.chrome.setBreadcrumbs(getScopedBreadcrumbs(breadCrumbs, params.history)), wrapInPage: true, }, dataSourceEnabled: !!dataSource, diff --git a/src/plugins/workspace/public/plugin.test.ts b/src/plugins/workspace/public/plugin.test.ts index 00f0f302ef8c..dc601137789b 100644 --- a/src/plugins/workspace/public/plugin.test.ts +++ b/src/plugins/workspace/public/plugin.test.ts @@ -186,7 +186,7 @@ describe('Workspace plugin', () => { expect(setupMock.application.register).toHaveBeenCalledWith( expect.objectContaining({ id: 'workspace_detail', - navLinkStatus: AppNavLinkStatus.visible, + navLinkStatus: AppNavLinkStatus.hidden, }) ); diff --git a/src/plugins/workspace/public/plugin.ts b/src/plugins/workspace/public/plugin.ts index 5452bdd7f2fd..dd4dc78fdf2c 100644 --- a/src/plugins/workspace/public/plugin.ts +++ b/src/plugins/workspace/public/plugin.ts @@ -115,6 +115,12 @@ export class WorkspacePlugin if (app.id === 'home' && isAllUseCase) { return { navLinkStatus: AppNavLinkStatus.hidden }; } + + // show the overview page in all use case + if (app.id === WORKSPACE_DETAIL_APP_ID && isAllUseCase) { + return { navLinkStatus: AppNavLinkStatus.visible }; + } + if (isAppAccessibleInWorkspace(app, currentWorkspace, registeredUseCases)) { return; } @@ -334,9 +340,7 @@ export class WorkspacePlugin title: i18n.translate('workspace.settings.workspaceDetail', { defaultMessage: 'Workspace Detail', }), - navLinkStatus: core.chrome.navGroup.getNavGroupEnabled() - ? AppNavLinkStatus.visible - : AppNavLinkStatus.hidden, + navLinkStatus: AppNavLinkStatus.hidden, async mount(params: AppMountParameters) { const { renderDetailApp } = await import('./application'); return mountWorkspaceApp(params, renderDetailApp);