mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Remember tab choice between logs explorer and discover (#194930)
Closes #193321 ## Summary The PR adds the redirection point when "Discover" menu item is clicked on the sidenav in serverless (or solution nav on stateful). Based on what tab between "Discover" or "Logs Explorer" the user clicked recently, "Discover" will point to that app/tab. Previously, "Discover" would always point to "Logs Explorer" on serverless and to "Discover" on stateful. In order to implement this, a temporary app `last-used-logs-viewer` is registered in `observability-logs-explorer` plugin whose only job is to read the last stored value in local storage and perform the redirection. Doing the redirection from a temporary app should help prevent triggering unnecessary telemetry and history entries. And it should be fairly easy to undo once context aware redirection is in place. ~With this implementation, only the behavior of user clicking "Discover" on the sidenav and clicking the tabs is affected and any deeplinks from other apps or direct links should work as is.~ The tab choice will be updated even if the apps are visited via url. https://github.com/user-attachments/assets/8a0308db-9ddb-47b6-b1a5-8ed70662040d --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
c53b2a8bb0
commit
fed9a19386
14 changed files with 287 additions and 10 deletions
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(<LogsExplorerTabs services={services} selectedTab={selectedTab} />);
|
||||
const { unmount } = render(<LogsExplorerTabs services={services} selectedTab={selectedTab} />);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 (
|
||||
<EuiTabs bottomBorder={false} data-test-subj="logsExplorerTabs">
|
||||
<EuiTab
|
||||
|
|
|
@ -159,6 +159,7 @@ export const applicationUsageSchema = {
|
|||
monitoring: commonSchema,
|
||||
'observability-log-explorer': commonSchema,
|
||||
'observability-logs-explorer': commonSchema,
|
||||
'last-used-logs-viewer': commonSchema,
|
||||
'observability-overview': commonSchema,
|
||||
observabilityOnboarding: commonSchema,
|
||||
observabilityAIAssistant: commonSchema,
|
||||
|
|
|
@ -5111,6 +5111,137 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"last-used-logs-viewer": {
|
||||
"properties": {
|
||||
"appId": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The application being tracked"
|
||||
}
|
||||
},
|
||||
"viewId": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "Always `main`"
|
||||
}
|
||||
},
|
||||
"clicks_total": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application since we started counting them"
|
||||
}
|
||||
},
|
||||
"clicks_7_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application over the last 7 days"
|
||||
}
|
||||
},
|
||||
"clicks_30_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application over the last 30 days"
|
||||
}
|
||||
},
|
||||
"clicks_90_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application over the last 90 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_total": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen since we started counting them."
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_7_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen over the last 7 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_30_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen over the last 30 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_90_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen over the last 90 days"
|
||||
}
|
||||
},
|
||||
"views": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"appId": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The application being tracked"
|
||||
}
|
||||
},
|
||||
"viewId": {
|
||||
"type": "keyword",
|
||||
"_meta": {
|
||||
"description": "The application view being tracked"
|
||||
}
|
||||
},
|
||||
"clicks_total": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the application sub view since we started counting them"
|
||||
}
|
||||
},
|
||||
"clicks_7_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the active application sub view over the last 7 days"
|
||||
}
|
||||
},
|
||||
"clicks_30_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the active application sub view over the last 30 days"
|
||||
}
|
||||
},
|
||||
"clicks_90_days": {
|
||||
"type": "long",
|
||||
"_meta": {
|
||||
"description": "General number of clicks in the active application sub view over the last 90 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_total": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application sub view is active and on-screen since we started counting them."
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_7_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_30_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
|
||||
}
|
||||
},
|
||||
"minutes_on_screen_90_days": {
|
||||
"type": "float",
|
||||
"_meta": {
|
||||
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"observability-overview": {
|
||||
"properties": {
|
||||
"appId": {
|
||||
|
|
|
@ -34,12 +34,22 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) {
|
|||
link: 'observability-overview',
|
||||
},
|
||||
{
|
||||
link: 'discover',
|
||||
title: i18n.translate('xpack.observability.obltNav.discover', {
|
||||
defaultMessage: 'Discover',
|
||||
}),
|
||||
// 'last-used-logs-viewer' is wrapper app to handle the navigation between observability-log-explorer and discover
|
||||
link: 'last-used-logs-viewer',
|
||||
breadcrumbStatus: 'hidden', // avoid duplicate "Discover" breadcrumbs
|
||||
renderAs: 'item',
|
||||
children: [
|
||||
{
|
||||
// This is to show "observability-log-explorer" breadcrumbs when navigating from "discover" to "log explorer"
|
||||
link: 'observability-logs-explorer',
|
||||
link: 'discover',
|
||||
children: [
|
||||
{
|
||||
// This is to show "observability-log-explorer" breadcrumbs when navigating from "discover" to "log explorer"
|
||||
link: 'observability-logs-explorer',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import { Router } from '@kbn/shared-ux-router';
|
||||
import {
|
||||
OBSERVABILITY_LOGS_EXPLORER_APP_ID,
|
||||
OBS_LOGS_EXPLORER_LOGS_VIEWER_KEY,
|
||||
} from '@kbn/deeplinks-observability';
|
||||
import { DISCOVER_APP_ID } from '@kbn/deeplinks-analytics';
|
||||
import { AppMountParameters, CoreStart } from '@kbn/core/public';
|
||||
|
||||
export const renderLastUsedLogsViewerRedirect = (
|
||||
core: CoreStart,
|
||||
appParams: AppMountParameters
|
||||
) => {
|
||||
ReactDOM.render(
|
||||
<Router history={appParams.history}>
|
||||
<LastUsedLogsViewerRedirect core={core} />
|
||||
</Router>,
|
||||
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 <></>;
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"@kbn/core-analytics-browser",
|
||||
"@kbn/react-hooks",
|
||||
"@kbn/logs-data-access-plugin",
|
||||
"@kbn/deeplinks-analytics",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue