diff --git a/packages/deeplinks/observability/constants.ts b/packages/deeplinks/observability/constants.ts index 45868fa3a16b2..25642ba69613a 100644 --- a/packages/deeplinks/observability/constants.ts +++ b/packages/deeplinks/observability/constants.ts @@ -11,6 +11,9 @@ export const LOGS_APP_ID = 'logs'; export const OBSERVABILITY_LOGS_EXPLORER_APP_ID = 'observability-logs-explorer'; +// TODO: Remove the app once context-aware switching between discover and observability logs explorer is implemented +export const LAST_USED_LOGS_VIEWER_APP_ID = 'last-used-logs-viewer'; + export const OBSERVABILITY_OVERVIEW_APP_ID = 'observability-overview'; export const METRICS_APP_ID = 'metrics'; diff --git a/packages/deeplinks/observability/deep_links.ts b/packages/deeplinks/observability/deep_links.ts index 088b9c866c03d..1253b4e889fcf 100644 --- a/packages/deeplinks/observability/deep_links.ts +++ b/packages/deeplinks/observability/deep_links.ts @@ -12,6 +12,7 @@ import { LOGS_APP_ID, METRICS_APP_ID, OBSERVABILITY_LOGS_EXPLORER_APP_ID, + LAST_USED_LOGS_VIEWER_APP_ID, OBSERVABILITY_ONBOARDING_APP_ID, OBSERVABILITY_OVERVIEW_APP_ID, SYNTHETICS_APP_ID, @@ -24,6 +25,7 @@ import { type LogsApp = typeof LOGS_APP_ID; type ObservabilityLogsExplorerApp = typeof OBSERVABILITY_LOGS_EXPLORER_APP_ID; +type LastUsedLogsViewerApp = typeof LAST_USED_LOGS_VIEWER_APP_ID; type ObservabilityOverviewApp = typeof OBSERVABILITY_OVERVIEW_APP_ID; type MetricsApp = typeof METRICS_APP_ID; type ApmApp = typeof APM_APP_ID; @@ -38,6 +40,7 @@ type InventoryApp = typeof INVENTORY_APP_ID; export type AppId = | LogsApp | ObservabilityLogsExplorerApp + | LastUsedLogsViewerApp | ObservabilityOverviewApp | ObservabilityOnboardingApp | ApmApp diff --git a/packages/deeplinks/observability/index.ts b/packages/deeplinks/observability/index.ts index 7185a4cfbcd6f..8bedc43f5d6a8 100644 --- a/packages/deeplinks/observability/index.ts +++ b/packages/deeplinks/observability/index.ts @@ -10,6 +10,7 @@ export { LOGS_APP_ID, OBSERVABILITY_LOGS_EXPLORER_APP_ID, + LAST_USED_LOGS_VIEWER_APP_ID, OBSERVABILITY_ONBOARDING_APP_ID, OBSERVABILITY_OVERVIEW_APP_ID, AI_ASSISTANT_APP_ID, diff --git a/packages/deeplinks/observability/locators/observability_logs_explorer.ts b/packages/deeplinks/observability/locators/observability_logs_explorer.ts index 037acb7d946ad..ae4cbc12cec6d 100644 --- a/packages/deeplinks/observability/locators/observability_logs_explorer.ts +++ b/packages/deeplinks/observability/locators/observability_logs_explorer.ts @@ -49,3 +49,6 @@ export interface ObsLogsExplorerDataViewLocatorParams extends DatasetLocatorPara */ id: string; } + +// To store the last used logs viewer (either of discover or observability-logs-explorer) +export const OBS_LOGS_EXPLORER_LOGS_VIEWER_KEY = 'obs-logs-explorer:lastUsedViewer'; diff --git a/src/plugins/discover/public/components/logs_explorer_tabs/logs_explorer_tabs.test.tsx b/src/plugins/discover/public/components/logs_explorer_tabs/logs_explorer_tabs.test.tsx index 1782dc7ad6ac7..e353fe1971ec9 100644 --- a/src/plugins/discover/public/components/logs_explorer_tabs/logs_explorer_tabs.test.tsx +++ b/src/plugins/discover/public/components/logs_explorer_tabs/logs_explorer_tabs.test.tsx @@ -10,10 +10,21 @@ import userEvent from '@testing-library/user-event'; import { render, screen } from '@testing-library/react'; import React from 'react'; +import { DISCOVER_APP_ID } from '@kbn/deeplinks-analytics'; +import { + ALL_DATASETS_LOCATOR_ID, + OBSERVABILITY_LOGS_EXPLORER_APP_ID, +} from '@kbn/deeplinks-observability'; import { discoverServiceMock } from '../../__mocks__/services'; import { LogsExplorerTabs, LogsExplorerTabsProps } from './logs_explorer_tabs'; import { DISCOVER_APP_LOCATOR } from '../../../common'; -import { ALL_DATASETS_LOCATOR_ID } from '@kbn/deeplinks-observability'; + +const mockSetLastUsedViewer = jest.fn(); +jest.mock('react-use/lib/useLocalStorage', () => { + return jest.fn((key: string, _initialValue: string) => { + return [undefined, mockSetLastUsedViewer]; // Always use undefined as the initial value + }); +}); const createMockLocator = (id: string) => ({ navigate: jest.fn(), @@ -46,11 +57,12 @@ describe('LogsExplorerTabs', () => { }, } as unknown as typeof discoverServiceMock; - render(); + const { unmount } = render(); return { mockDiscoverLocator, mockLogsExplorerLocator, + unmount, }; }; @@ -86,4 +98,14 @@ describe('LogsExplorerTabs', () => { await userEvent.click(getDiscoverTab()); expect(mockDiscoverLocator.navigate).toHaveBeenCalledWith({}); }); + + it('should update the last used viewer in local storage for selectedTab', async () => { + const { unmount } = renderTabs('discover'); + expect(mockSetLastUsedViewer).toHaveBeenCalledWith(DISCOVER_APP_ID); + + unmount(); + mockSetLastUsedViewer.mockClear(); + renderTabs('logs-explorer'); + expect(mockSetLastUsedViewer).toHaveBeenCalledWith(OBSERVABILITY_LOGS_EXPLORER_APP_ID); + }); }); diff --git a/src/plugins/discover/public/components/logs_explorer_tabs/logs_explorer_tabs.tsx b/src/plugins/discover/public/components/logs_explorer_tabs/logs_explorer_tabs.tsx index 1eec001464d2a..c7082c21344ac 100644 --- a/src/plugins/discover/public/components/logs_explorer_tabs/logs_explorer_tabs.tsx +++ b/src/plugins/discover/public/components/logs_explorer_tabs/logs_explorer_tabs.tsx @@ -8,9 +8,16 @@ */ import { EuiTab, EuiTabs, useEuiTheme } from '@elastic/eui'; -import { AllDatasetsLocatorParams, ALL_DATASETS_LOCATOR_ID } from '@kbn/deeplinks-observability'; +import { DISCOVER_APP_ID } from '@kbn/deeplinks-analytics'; +import { + AllDatasetsLocatorParams, + ALL_DATASETS_LOCATOR_ID, + OBS_LOGS_EXPLORER_LOGS_VIEWER_KEY, + OBSERVABILITY_LOGS_EXPLORER_APP_ID, +} from '@kbn/deeplinks-observability'; import { i18n } from '@kbn/i18n'; -import React, { MouseEvent } from 'react'; +import React, { MouseEvent, useEffect } from 'react'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; import { DiscoverAppLocatorParams, DISCOVER_APP_LOCATOR } from '../../../common'; import type { DiscoverServices } from '../../build_services'; @@ -29,6 +36,10 @@ export const LogsExplorerTabs = ({ services, selectedTab }: LogsExplorerTabsProp const discoverUrl = discoverLocator?.getRedirectUrl(emptyParams); const logsExplorerUrl = logsExplorerLocator?.getRedirectUrl(emptyParams); + const [lastUsedViewer, setLastUsedViewer] = useLocalStorage< + typeof DISCOVER_APP_ID | typeof OBSERVABILITY_LOGS_EXPLORER_APP_ID + >(OBS_LOGS_EXPLORER_LOGS_VIEWER_KEY, OBSERVABILITY_LOGS_EXPLORER_APP_ID); + const navigateToDiscover = createNavigateHandler(() => { if (selectedTab !== 'discover') { discoverLocator?.navigate(emptyParams); @@ -41,6 +52,16 @@ export const LogsExplorerTabs = ({ services, selectedTab }: LogsExplorerTabsProp } }); + useEffect(() => { + if (selectedTab === 'discover' && lastUsedViewer !== DISCOVER_APP_ID) { + setLastUsedViewer(DISCOVER_APP_ID); + } + + if (selectedTab === 'logs-explorer' && lastUsedViewer !== OBSERVABILITY_LOGS_EXPLORER_APP_ID) { + setLastUsedViewer(OBSERVABILITY_LOGS_EXPLORER_APP_ID); + } + }, [setLastUsedViewer, lastUsedViewer, selectedTab]); + return ( { + ReactDOM.render( + + + , + appParams.element + ); + + return () => { + ReactDOM.unmountComponentAtNode(appParams.element); + }; +}; + +export const LastUsedLogsViewerRedirect = ({ core }: { core: CoreStart }) => { + const location = useLocation(); + const path = `${location.pathname}${location.search}`; + const [lastUsedLogsViewApp] = useLocalStorage< + typeof DISCOVER_APP_ID | typeof OBSERVABILITY_LOGS_EXPLORER_APP_ID + >(OBS_LOGS_EXPLORER_LOGS_VIEWER_KEY, OBSERVABILITY_LOGS_EXPLORER_APP_ID); + + if ( + lastUsedLogsViewApp && + lastUsedLogsViewApp !== DISCOVER_APP_ID && + lastUsedLogsViewApp !== OBSERVABILITY_LOGS_EXPLORER_APP_ID + ) { + throw new Error( + `Invalid last used logs viewer app: "${lastUsedLogsViewApp}". Allowed values are "${DISCOVER_APP_ID}" and "${OBSERVABILITY_LOGS_EXPLORER_APP_ID}"` + ); + } + + useEffect(() => { + if (lastUsedLogsViewApp === DISCOVER_APP_ID) { + core.application.navigateToApp(DISCOVER_APP_ID, { replace: true, path }); + } + + if (lastUsedLogsViewApp === OBSERVABILITY_LOGS_EXPLORER_APP_ID) { + core.application.navigateToApp(OBSERVABILITY_LOGS_EXPLORER_APP_ID, { replace: true, path }); + } + }, [core, path, lastUsedLogsViewApp]); + + return <>; +}; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/plugin.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/public/plugin.ts index 798a03da0ebdf..2e6ab0aeeaa0f 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/plugin.ts @@ -97,6 +97,22 @@ export class ObservabilityLogsExplorerPlugin }, }); + // App used solely to redirect to either "/app/observability-logs-explorer" or "/app/discover" + // based on the last used app value in localStorage + core.application.register({ + id: 'last-used-logs-viewer', + title: logsExplorerAppTitle, + visibleIn: [], + mount: async (appMountParams: AppMountParameters) => { + const [coreStart] = await core.getStartServices(); + const { renderLastUsedLogsViewerRedirect } = await import( + './applications/last_used_logs_viewer' + ); + + return renderLastUsedLogsViewerRedirect(coreStart, appMountParams); + }, + }); + core.analytics.registerEventType(DATA_RECEIVED_TELEMETRY_EVENT); // Register Locators diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json b/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json index 6ea751aaed3de..be2b3c9efdff6 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/tsconfig.json @@ -50,6 +50,7 @@ "@kbn/core-analytics-browser", "@kbn/react-hooks", "@kbn/logs-data-access-plugin", + "@kbn/deeplinks-analytics", ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/serverless_observability/public/navigation_tree.ts b/x-pack/plugins/serverless_observability/public/navigation_tree.ts index e3c61ec0b29e3..5df900ee46812 100644 --- a/x-pack/plugins/serverless_observability/public/navigation_tree.ts +++ b/x-pack/plugins/serverless_observability/public/navigation_tree.ts @@ -24,7 +24,7 @@ export const navigationTree: NavigationTreeDefinition = { title: i18n.translate('xpack.serverlessObservability.nav.discover', { defaultMessage: 'Discover', }), - link: 'observability-logs-explorer', + link: 'last-used-logs-viewer', // avoid duplicate "Discover" breadcrumbs breadcrumbStatus: 'hidden', renderAs: 'item', diff --git a/x-pack/test_serverless/functional/test_suites/observability/navigation.ts b/x-pack/test_serverless/functional/test_suites/observability/navigation.ts index ce6b68cd618a1..bcd9c031714f7 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/navigation.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/navigation.ts @@ -35,9 +35,10 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await svlCommonNavigation.sidenav.expectSectionClosed('project_settings_project_nav'); // navigate to the logs explorer tab by default - await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'observability-logs-explorer' }); + // 'last-used-logs-viewer' is wrapper app to handle the navigation between logs explorer and discover + await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'last-used-logs-viewer' }); await svlCommonNavigation.sidenav.expectLinkActive({ - deepLinkId: 'observability-logs-explorer', + deepLinkId: 'last-used-logs-viewer', }); await svlCommonNavigation.breadcrumbs.expectBreadcrumbExists({ deepLinkId: 'observability-logs-explorer',